Conversation
Three things found standing the whole stack up for the first QA pass: * api/tests/test_cue_rss.py pinned its feed fixture to a fixed 2026-07-24 pubDate, but ingest_once() also PRUNES at keep_days=14 — so the test began failing on 2026-08-08, 14 days after it was written, on main and with no code change. Re-stamp the ingest fixture relative to now; the parse tests that assert an exact date keep the fixed one. * docker-compose.yml is the deployed shape: `C:/docker/tenir/...` bind mounts and a local GPU parakeet server. Neither exists on a Linux dev box, so the stack could not be brought up to QA it at all. docker-compose.qa.yml is an overlay that swaps bind mounts for named volumes, drops the GPU service, moves the published ports off the defaults and runs under its own project name, so a QA stack can never adopt or clobber a real one. * scripts/functional_test.py hardcoded localhost:8080 (now TENIR_BASE) and raced the server on `session.end`: finalization is offloaded to threads, so a GET issued the instant the socket closes legitimately reads "live". Poll for "ready" instead of asserting on the first read. Verified: api 430 passed (95% cov); npm typecheck/test/build green; veiller 115 bun tests green; suites re-run under faked 2026-12/2027/2028 clocks and under UTC+14/-10 timezones to rule out further time bombs; functional_test.py 10/10 against the running stack.
Two auth/tenancy holes the QA pass reproduced against a running stack.
1. HIGH — a client-supplied `sessionId` could address another household's
retained audio. `session.start` takes an optional resume id and it was used
verbatim as both the conversation key and the audio object key
(`{household}/{id}.wav`). An id shaped `../other-hh/<their-id>` never leaves
the audio root, so the store's escape check passed it: the api read that
household's WAV back as this session's resume offset (leaking its duration,
and reporting resumed=true), then on session.end prepended and rewrote it.
Reproduced end to end — household B doubled household A's stored recording,
64044 -> 128044 bytes, md5 changed.
Fixed at two layers. The WS boundary now honours only ids the server could
have issued (canonical uuid4 text); anything else is logged, counted
(`sessions.bad_resume_id`) and dropped, so the client still gets a working
session under a fresh id. The audio store independently refuses a key that
is not exactly one household segment plus one id segment — `resolve()`
collapses `..` before any depth check can see it, so the raw segments are
validated first.
2. MEDIUM — deleting a user did not revoke their token. Principals resolved on
signature + expiry alone, and tokens last 30 days by default (sliding
renewal keeps an active device's fresh indefinitely), so a removed member
kept full household access for up to a month. Every authenticated entry
point — REST, the query-token audio route, and the WS handshake — now
resolves through `principal_from_live_token`, which also checks the account
still exists. Renewal already did this lookup; it was the base access path
that didn't.
Tests: new `test_session_id_isolation.py` drives the real attack over the WS
and asserts the victim's audio is byte-identical, plus store-level cases for
the sideways hop and the key builder. `test_deleted_user_token_is_revoked_
immediately` covers REST, the query-token route and the socket. The store
tests fail against the old code and pass against the new; the previous test
that asserted a deleted user's token still worked now asserts it doesn't.
api: 436 passed, 95.38% coverage. Re-ran the live repro against the fixed api:
the traversal id is refused, B gets a server-issued uuid, A's file untouched.
…K-236) The critical one first. * CRITICAL — a dropped session that nobody resumed was never finalized and its audio was thrown away. `_grace_close()` calls `close()`, and `close()` cancelled `self._grace_task` unconditionally — i.e. the task that was calling it. The CancelledError landed on the first await inside close() (the pump join), so `_persist()` never ran: the conversation stayed `live` forever and the entire retained audio buffer went with the process. Reproduced with 64 kB of captured audio discarded. This is precisely the path even/README and even/src/lens/controller.ts document as the safety net for an abnormal exit — phone out of range, app killed, dead BLE link — so every one of those lost the recording. close() now spares the grace task when that task IS the caller. * Rows already stranded `live` by an OOM kill or host reboot never healed — nothing revisited them, so they showed as permanently recording in every client's history. `finish_stale()` sweeps them once at startup, before any new session registers; the SQL version back-dates `ended_at` from the last segment's offset rather than stamping "now". * Bearer tokens were written to the access log in cleartext. The WS handshake and the audio download both carry the token in the query string (neither caller can set a header), and uvicorn logs the full request line — so every connect and every download put a live 30-day credential into an unrotated container log. The URL constraint is real, so redact on the way out instead. * `assert_secure_auth_config` only compared against the literal shipped default, so `API_AUTH_SECRET=""` booted — and an empty HMAC key makes an admin token for any household trivially forgeable. Now requires a non-blank secret of at least 32 chars. * Config accepted values that boot "healthy" and fail as something else: `API_STT_BACKEND=banana` served a clean /status and then failed every session with a generic "could not start session"; `API_AUTH_TOKEN_TTL_SECONDS=0` issued a token from /auth/login that was already expired on the next request; `API_STATUS_PROBE_INTERVAL_SECONDS=0` left /status green with nothing probed. The backend selector and every duration are now validated at config time, matching what the persistence and audio selectors already did. Tests: test_qa_hardening.py covers all five, including that an explicit close still cancels a pending grace task. The conftest secret grew to satisfy the new minimum. api: 465 passed, 95.31% coverage.
…(XERK-236) CRITICAL * Android's release APK could not reach any http:// server. The release manifest declared neither `usesCleartextTraffic` nor a network security config, and targetSdk 34 means the platform blocks cleartext by default — only the DEBUG manifest allowed it. So the APK that CI publishes, and that the in-app updater installs, could not talk to the project's own documented deployment (`docker compose up` on :8080 over http, and the seeded ws://localhost:8080/ws): sign-in failed with a bare "could not reach the server" and no request ever left the device. Invisible in development, because `npm run android` builds debug. Adds a network security config that permits cleartext deliberately — the server is arbitrary user-supplied infrastructure we cannot pin. * Any link could silently switch the signed-in web account. `adoptTokenFromUrl` took any `#token=` value, overwrote the stored token with no validation or consent, and scrubbed the fragment so nothing looked wrong. The victim's history vanished, everything they recorded afterwards landed in the attacker's household, and `#token=garbage` destroyed a working session just as quietly. It now refuses to replace an existing session (the XERK-82 glasses handoff into a signed-out browser still works), and strips the fragment either way. The residual — a handoff link logging a fresh browser into the attacker's account — needs a server-issued one-time code; filed in qa.md. HIGH * A fatal error left the microphone open with the UI still claiming to record. `captureSession` recorded the message and nothing else, so `running` stayed true: Stop/Pause stayed up, the browser's recording indicator and Android's mic foreground service stayed lit, and the glasses lens read "listening…" while PCM streamed into a socket that no longer had a session. For a product whose promise is "recorded and stored", believing you are being recorded while nothing is captured is the worst failure available. A fatal error now tears the capture down through the normal stop path. The two glasses clients additionally stop on any non-auth error that arrives before a session exists — which is exactly what the api sends when the STT backend is down. * An unbounded, backoff-free re-login storm. `connect()` reset the one-shot `reauthAttempted` guard, and the unauthorized handler calls `connect()` after a silent re-login — so the guard was re-armed every time round the loop. A server that keeps rejecting the token got 167 logins + 167 upgrades in 15 s (~5000 in 10 s with no network latency). `onReady` already resets the guard, which is the correct place: a session that actually started is the only proof the re-auth worked. Same bug, same fix, in even/ and veiller/. * `displayServerUrl` dropped an insecure `ws://` scheme, but a bare host normalizes back to `wss://` — so pressing Connect in Settings without editing anything silently upgraded a plain-HTTP self-hosted server and signed the user out. The first-run seed had the same trap. * Mobile treated any transport failure as "signed out": a briefly-unreachable server dropped the user onto an empty login screen, though their token was fine and relaunching later signed them straight back in. It now says the server is unreachable and offers a retry. * Three web panels claimed everything was fine while the API failed: Status rendered "no components are configured to monitor" on a 500 (the one screen that exists to report trouble), Users rendered a blank page on any load error (an admin reads that as "my household is empty"), and a failed delete left a phantom row that could never be cleared. Deleting the conversation you are recording right now was also offered unwarned — it took the row out from under the running session and discarded everything said afterwards; the control is now disabled while a conversation is live. Verified: npm typecheck/test/build green (client-core 137, even 288, mobile 163, web 108); veiller 115 bun tests + build. The captureSession and web config regressions fail against the unfixed code. Rebuilt the app image and re-ran both end-to-end suites against the live stack: functional_test 10/10, cross-client walk 11/11. Token redaction confirmed in the running container's logs — the real token appears 0 times, every request line reads `token=<redacted>`.
…ERK-236)
Release pipeline
* Every minor CHANGELOG rollup has been empty since 0.2.0 — see CHANGELOG.md,
where 0.2.0 through 0.6.0 all read "_No changes._" across 117 tags.
changelog-cli.js documents BASE_MAJOR/BASE_MINOR as the PRE-bump line (the one
being closed), but plan.js exported the post-bump base, so the start tag was
the line being OPENED — a tag that does not exist yet — and the range fell
back to empty. Proven both ways: base 0.7 gives "0 entries since v0.7.0",
base 0.6 gives "10 entries since v0.6.0". plan.js now also exports the
pre-bump base and release.yml passes that to the rollup, while VERSION still
gets the bumped one.
* schema.sql mapped to no component, so a schema-only merge cut no release and
never reached a running deployment — even though api/Dockerfile bakes it into
the image and the store applies it on every pool open (that is how an existing
data dir gains an additive column). It now maps to api, triggers a release,
and runs the api gate, which already has tests for it.
* docker-compose*.yml, schema.sql and litellm/config.yaml ran under no PR gate
at all. Added to api.yml's paths.
* version.compare could be swapped for a lexical string compare with the whole
42-test suite staying green, despite the code comment ("NOT lexical — v0.3.10
> v0.3.9") calling out the hazard. Lexical ordering picks v0.1.9 over v0.1.29,
which hands the release the wrong diff range and refuses legitimate versions.
Pinned with double-digit cases; the mutation now fails 2 tests.
* The release.yml/changes.js drift guard parsed only consecutive `- "…"` lines,
so a comment inside the paths list silently truncated it — hiding the very
drift the test exists to catch. It now skips comments.
* Pinned hadolint (was `:latest` in a gate) and added least-privilege
`permissions: contents: read` to core.yml and contract-drift.yml, the only two
workflows without one.
Deploy
* The root compose file could not start on Linux or macOS at all: every bind
mount was a literal Windows path, so the README's own quick start died with
"invalid volume specification". Parameterized as ${TENIR_DATA_DIR:-C:/docker/
tenir}, which leaves the Windows deploy host byte-identical.
* Postgres and LiteLLM published on 0.0.0.0 with hardcoded tenir/tenir and no
TLS — every household's transcripts and the users table were readable and
writable from the LAN. Both now bind loopback by default; nothing outside the
compose network needs them.
* The app healthcheck probed /health, which is pure liveness and answers "ok"
with Postgres down — so the container stayed green while logins hung 30 s and
500ed, and autoheal and depends_on saw healthy. It probes /ready now. (This
immediately paid for itself: it caught a non-root experiment breaking audio
writes, which the old check would have reported as healthy.)
* Added restart: unless-stopped (nothing came back after a host reboot), 10m×5
log rotation (json-file was unbounded, and the logs carry request lines), and
stop_grace_period: 30s — the api waits up to 15 s finalizing a session and
docker's default 10 s SIGKILLed it mid-persist.
* .env rode along in every build context (verified with a COPY . probe).
Excluded, with the Android keystore.
* Dropped the setuptools build tree and the duplicate source copy from the
runtime image. Running as a non-root user is NOT included: API_AUDIO_DIR is a
root-owned bind mount on deployed hosts, so changing the runtime uid silently
breaks audio writes until an operator chowns it. Filed in qa.md with what a
safe version needs.
* QA overlay: bound to loopback, documented that `up --build` builds the GPU
image despite replicas: 0, and dropped a dead depends_on override.
Verified: release-script tests 42→44 pass; lexical-compare mutation now caught.
Rebuilt the image and re-ran everything against the live stack — api 465 passed
(95.31%), functional_test 10/10, cross-client 11/11, /ready true, healthcheck
healthy. Hard-killed the container mid-capture with SIGKILL and restarted: the
row that would have been stuck "live" forever came back `ready` with a
back-dated endedAt.
The ticket asks for whatever makes the next QA pass easier. Two things do: the setup that actually works (model-free stub STT captions from silence, the compose overlay, per-agent project names, a browser without sudo, the emulator incantation, the Veiller simulator gotcha), and the traps that cost this pass real time (time-bomb fixtures, session.end finalizing asynchronously, jsdom dropping a frame sent in the same tick as close()). It also carries the triaged backlog: every reproduced finding the pass did not fix, with file references — feature-shaped gaps (no pagination, no export UI, no change-password), security follow-ups (the remaining #token= exposure, plaintext credentials on the clients, the root image), correctness/UX defects, and the test-suite escapes found by mutation testing.
The adversarial gate returned FAIL on my own changes. Everything it found: * **`npm run typecheck` exited 2** — `Core` and `Web` would have gone red on the PR. The regression test I added for the fatal-error fix didn't compile (`code` widened to `string`; only `type` had `as const`). I missed it because I ran the gate as `npm run typecheck 2>&1 | tail`, and a pipe swallows the exit status. Now typed against the generated `ErrorMessage`, and qa.md says to check `$?`. * **The `/ready` healthcheck was a regression, not a fix.** This host runs autoheal in `label=all` mode, so unhealthy means restart — a Postgres blip bounced the api every ~60-75 s for the length of the outage, and every bounce killed live captures and their in-memory audio: exactly the loss the grace-window and stale-sweep fixes exist to prevent. Reverted to `/health` with the reasoning written down, since "probe /ready instead" is the obvious thing for the next person to try. * **The token redaction was bypassable, twice.** Starlette percent-decodes query keys, so `?%74oken=` authenticates and did not match the literal `token=` regex — a live 30-day admin token in the container log. I fixed the regex to match on the decoded parameter name, and the container STILL leaked it, because the filter kept a `"token=" in arg` fast-path guard that skipped the record before the corrected regex ever ran. The function's own unit test passed throughout. Both fixed; the new test drives the whole filter, and the running container now shows 0 occurrences across `token`/`%74oken`/`%74%6Fken` /`TOKEN`. * **`API_SESSION_RESUME_GRACE_SECONDS=0` became a hard boot failure.** 0 means "resume disabled" — config.py documents it and `detach()` has a branch for it, which my validator turned into dead code. Now `>= 0` for that field alone. * **`_persist()` was unguarded**, so an audio-store failure propagated out of `close()` and skipped `finish()` — losing the recording AND the record of it. Audio retention is now best-effort; finalizing is not. * **A NUL byte in a REST conversation id** reached psycopg and returned 500 with a traceback. The WS path got a shape guard in the first round and the REST path got nothing; now both reject a non-uuid id as 404. * **Deleting a user didn't end their in-flight capture socket** — auth is checked at the handshake only, so a removed member kept recording into the household. Their live sessions are now closed (finalized normally, so nothing captured is lost). * **Android's first run opened on "Can't reach your server"** — telling a brand-new user they may still be signed in. Gated on a stored token existing. * **8 of 22 mutations of my own fixes survived their suites**, and three fixes shipped with no test at all. Added: web History live-delete + failed-delete reload, Status non-network failure, Users load error; even/ and veiller/ reauth-storm and non-auth-error teardown; plan.js's outputs (the CHANGELOG fix itself was untested, so the "_No changes._" bug could walk right back in); and the redaction *installation* and startup stale-sweep *wiring*, which were both correct and unreachable-if-unwired. Each was verified by reverting the fix and watching the new test go red. * Two API tests **hung** instead of failing when mutated, burning the CI timeout rather than reporting; their receive loops now have deadlines. `finish_stale` now documents that it assumes one api process per database. Also verified by the gate and unchanged: the traversal fix against 16 hostile ids and a real second household, the grace-window fix against a real TCP abort with SO_LINGER 0 and at four grace-boundary offsets, deleted-token revocation on all three surfaces, the stale sweep against a real SIGKILL, and the release APK signing in over http:// on the emulator and driving a full capture. Gates (by exit code, not tail): api 467 passed / 95.12% (gate 85%); npm typecheck, test and build all exit 0 (client-core 137, even 292, mobile 163, web 115); veiller typecheck + 118 tests + build; release scripts 48. functional_test 10/10 and the cross-client walk 11/11 against the rebuilt image; NUL-byte paths now 404 where they were 500.
Both were mine, and both were caught by gates doing their job: * `hadolint/hadolint:2.14.0` does not exist — I pinned a tag I hadn't checked, and the lint job died on `manifest unknown` rather than on anything about the Dockerfile. Pinned to `v2.12.0`, which is real; verified locally against both Dockerfiles (exit 0, warnings only). * `api/tests/test_no_committed_secrets.py` flagged the percent-encoded literal in my new redaction test as a generated-looking password. That scanner is correct — an opaque `'%74%6Fken'` is exactly the shape it exists to catch — so the encoded spellings are now built from "token" instead of written out, which also reads better. api: 471 passed, 95.12% coverage. npm typecheck/test and the release scripts all exit 0.
… (XERK-236)
The GPU STT server came back, so the biggest hole in this PR's verification is
now filled: everything below ran against the real `nvidia/parakeet-tdt-0.6b-v3`
(TrueNAS, :9401) and the real `gpt-oss:120b` (:9402), driven with synthesized
speech rather than the silence the stub accepts. No household audio was used.
Two real gaps this exposed:
* **`revoke()`: deleting a user closed their session but not their socket.** The
first round finalized the Session, which left the WS handler parked in
receive() and still feeding audio into a session that no longer existed — a
removed member went on streaming until they chose to hang up. Only visible
driving a real socket; the unit test couldn't see it because TestClient runs
WS and HTTP on separate event loops. The endpoint now registers how to drop
its transport, and revoke() finalizes FIRST and hangs up second, so the
revoked member keeps what they had already said. Verified live: socket closed
with 1008, token 401s, recording finalized.
* **`scripts/functional_test.py` could only pass against the stub.** Its whole
stated job is smoke-testing the DEPLOYED stack — which runs parakeet — but it
sends silence and asserted a `caption.final`, so it reported a failure for the
real backend behaving exactly as designed. It now reads `stt_backend` from
/health and asserts what is true of each: 10/10 against parakeet, and it says
plainly that silence exercises the capture path, not accuracy.
What the real stack now demonstrates, end to end:
* three spoken turns → one final each, first partial under 3 s, **3.2 % word
error** against the script (the only miss: "Tenir" heard as "Tenure"),
segments time-ordered and non-overlapping, transcript searchable by a spoken
word, audio retained and replayable as a valid WAV spanning the session.
* **the critical grace-window fix, with real audio**: a socket aborted with
SO_LINGER 0 and no close frame — a dead BLE link — still finalized, with the
real transcript ("This recording must survive an abrupt disconnection.") and
its audio intact.
* **the traversal fix against a real recording**: the victim's WAV is
byte-identical after the attack, the id is neither echoed nor resumed.
* **real cues**: an accurate, grounded cue off a spoken exchange about Jupiter.
* **real translation**: a Spanish turn detected as `es` and translated to
"Hello, good morning. We're going to the beach tomorrow afternoon with the
family.", persisted on its segment, with `translation.done` closing the run.
One thing that looked like a product bug and was not: my first Spanish fixture
wrote "manana" without the ñ to keep the source ASCII. Parakeet transcribed it
faithfully as "maana", which drops `stt/langid.py` below its evidence floor, so
the turn came back lang=None and no translation fired. The detector is right;
the fixture was wrong. qa.md now warns about it, and records that STT and the
LLM live on two different hosts (the workspace CLAUDE.md has the STT address
wrong).
api: 474 passed, 95.14% coverage. npm typecheck/test, veiller and the release
scripts all exit 0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket: XERK-236 — "Tenir 1st QA pass".
Five adversarial QA agents swept the app front to back (api, web, Even G2 glasses, Android, Veiller miniapp, deploy/CI), each driving a real running stack rather than reading code. They produced ~60 reproduced findings. This PR fixes every critical and high, plus the security-relevant mediums; the rest are triaged into
qa.mdwith file references and repro steps.A sixth QA agent then ran as an adversarial gate on these fixes and returned FAIL. Everything it found is fixed here too (last commit).
The five worst
_grace_close()callsclose(), andclose()cancelledself._grace_task— the task calling it. TheCancelledErrorlanded on the first await, so_persist()never ran: the conversation stayedliveforever and the whole retained buffer went with the process. This is exactly the patheven/README.mddocuments as the safety net for an abnormal exit — phone out of range, app killed, dead BLE link — so every one of those lost the recording.http://server. NousesCleartextTraffic, no network security config,targetSdk 34— only the debug manifest allowed cleartext. The published APK could not talk to this project's own documented deployment. Invisible in dev, becausenpm run androidbuilds debug.http://10.0.2.2and drove a full captureadoptTokenFromUrltook any#token=value, overwrote the stored token with no consent, and scrubbed the fragment. The victim's history vanished, everything they recorded afterwards landed in the attacker's household.#token=garbagedestroyed a session just as quietly.sessionIdshaped../other-hh/<their-id>never leaves the audio root, so the store's escape check passed it: the api read another household's WAV as this session's resume offset, then prepended and rewrote it.runningstayed true across web, Android and both glasses clients. For a product whose promise is "recorded and stored", believing you are being recorded while nothing is captured is the worst failure available.live → endedAlso fixed
Auth / security — deleted users kept up to 30 days of access (no revocation on any surface, including the capture socket);
API_AUTH_SECRET=""booted and made admin tokens trivially forgeable; 30-day bearer tokens were written to unrotated container logs in cleartext; Postgres and LiteLLM published on0.0.0.0with hardcodedtenir/tenir; a NUL byte in a REST id returned 500 with a traceback.Correctness — an unbounded backoff-free re-login storm (167 logins in 15 s against a real api,
even/+veiller/); rows strandedliveby an OOM kill or reboot never healed;displayServerUrlsilently upgradedws://→wss://and signed Android users out; any transport failure showed mobile users an empty login screen; three web panels reported "everything is fine" while the API was failing; deleting the conversation you were recording was offered unwarned and discarded everything said afterwards.Config —
API_STT_BACKEND=bananabooted "healthy" then failed every session;API_AUTH_TOKEN_TTL_SECONDS=0issued already-expired tokens; probe interval 0 gave a falsely-green/status.Release pipeline — every minor CHANGELOG rollup has been empty since 0.2.0 (see
CHANGELOG.md: 0.2.0–0.6.0 all say "No changes." across 117 tags).release.ymlpassed the post-bump base tochangelog-cli.js, so the start tag was the line being opened — one that doesn't exist — and the range was empty. Also:schema.sqlships inside the api image but mapped to no component, so a schema-only merge cut no release and never reached a deployment;version.comparecould be swapped for a lexical compare with all 42 tests green.Infra — the root compose file could not start on Linux/macOS at all (
invalid volume specificationon the Windows bind mounts), so the README quick start was broken and the stack could not be QA'd;.envrode along in every build context; no restart policy, no log rotation, and a 10 s stop grace that SIGKILLed the api mid-persist.What the gate caught in my own fixes
Its FAIL verdict was correct and is worth reading:
npm run typecheckexited 2 —CoreandWebwould have been red on this PR. My own new test didn't compile. I missed it because I ran the gate as... | tail, and a pipe swallows the exit status.qa.mdnow says to check$?./readyhealthcheck was a regression, not a fix. This host runs autoheal inlabel=allmode, so a Postgres blip bounced the api every ~60–75 s — each bounce killing live captures and their audio, i.e. the exact loss two other fixes here exist to prevent. Reverted to/health, with the reasoning written down since "probe /ready instead" is the obvious next idea.?%74oken=authenticates (Starlette decodes query keys) and evaded the literal regex. I fixed the regex — and the container still leaked it, because the filter kept a"token=" in argfast-path guard that skipped the record before the corrected regex ran. The function's unit test passed the whole time.Verification
All by exit code, not tail output.
npm typecheck/test/buildall exit 0 (client-core 137, even 292, mobile 163, web 115). veiller: typecheck + 118 tests + build. Release scripts: 48.docker-compose.qa.yml):scripts/functional_test.py10/10; a cross-client walk (glasses-shaped WS capture → web/mobile REST read, export, WAV download, range request, search, delete) 11/11.SO_LINGER 0, plus four grace-boundary offsets — one conversation, audio intact, no hang, no double-persist.docker kill -s KILLmid-capture; the row that would have been stucklivecame backreadywith a back-datedended_at. Idempotent;readyrows untouched.token,%74oken,%74%6Fken,TOKEN.turma228AVD, signed in overhttp://10.0.2.2, drove a capture tosession … ready, no cleartext exception in logcat.TZ=Pacific/Kiritimati/Pacific/Honolulu.docker compose configrenders the Windows deploy shape byte-identically whenTENIR_DATA_DIRis unset.Deliberately not done
API_AUDIO_DIRbind mount, and the new/readyprobe caught it. Shipping it would silently lose recordings on the deployed host until someonechowned the data dir. Needs an entrypoint that fixes ownership before dropping privileges, plus a release note — noted inapi/Dockerfileandqa.md.#token=exposure into a signed-out browser (needs a server-issued one-time code), plaintext credential storage on the clients, and the LOW UX items. All inqa.mdwith repro steps.Verified against the real models
The GPU STT server came back mid-review, so this is no longer a stub-only pass. Everything below ran against the real
nvidia/parakeet-tdt-0.6b-v3(TrueNAS:9401) and the realgpt-oss:120b(:9402), driven with synthesized speech — no household audio was used.SO_LINGER 0and no close frame — a dead BLE link — still finalized, with its real transcript and audio intact.esand translated to English, persisted on its segment, withtranslation.doneclosing the run.This exposed two real gaps, now fixed (last commit):
Session, which left the WS handler parked inreceive()still feeding audio into a session that no longer existed — a removed member went on streaming until they chose to hang up. Only visible driving a real socket;TestClientruns WS and HTTP on separate event loops, so the unit test could not see it.scripts/functional_test.pycould only pass against the stub. Its stated job is smoke-testing the deployed stack — which runs parakeet — but it sends silence and asserted acaption.final, so it reported a failure for the real backend behaving as designed. It is now backend-aware: 10/10 against parakeet.One thing that looked like a product bug and was not: my first Spanish fixture wrote "manana" without the ñ to keep the source ASCII. Parakeet transcribed it faithfully as "maana", which drops
stt/langid.pybelow its evidence floor, so the turn came backlang=Noneand no translation fired. The detector is correct; the fixture was wrong.qa.mdwarns about it.Still not verified
Music ID (needs a song playing;
shazamioreaches an external service). iOS (no Xcode project in the repo), physical G2 hardware, non-Chromium browsers, and the release workflow end to end (simulated locally against real tags; nothing pushed or tagged).qa.mdThe ticket asks for whatever makes the next pass easier.
qa.mdcarries the setup that actually works (stub STT yields captions from silence — the single most useful fact), the compose overlay, per-agent project names, a browser without sudo, the emulator incantation, the Veiller simulator gotcha, the traps that cost this pass real time, and the full triaged backlog.