Fix the voice loop: a deaf microphone, his own voice interrupting him, and everything said three times - #32
Merged
ethanplusai merged 1 commit intoSep 6, 2026
Conversation
Six things from a day of using him by voice on a room microphone rather than a headset. Every one of them was found in a log and is covered by a test naming the failure it protects against. THE MICROPHONE It could go deaf and say nothing about it. Chrome ends a `continuous` recognition session on its own and `safeStart` swallowed the InvalidStateError thrown when the engine is between states — by which time `onend` has already fired, so nothing retried and it never listened again. Worse, a session can report `listening`, attach audio, and simply return nothing: measured live at 45 seconds of a live capture with no results, during which nothing anywhere said anything was wrong. `createMicMonitor` holds its own getUserMedia stream open for the life of the page — no session, no timeout, no restarts — and measures the signal itself. Sound going in with nothing coming out is now a named failure that rebuilds the recogniser within three seconds, and a meter in the corner answers "is it even hearing me?" without a log or a console. What the engine is doing is tracked (`onstart`/`onaudiostart`) rather than assumed, and a watchdog covers the two shapes events cannot: nothing listening, and listening with no audio. The recogniser is also rotated before Chrome's ~8s no-speech timeout can tear it down, but ONLY in silence: Chrome allows one live recogniser, so starting a second aborts the first, and an aborted session never emits the final result for whatever was being said. Rotating mid-sentence destroyed it. Measured: interims for a whole sentence arrived, rotation fired 17ms after the last one, and nothing reached the server. HIS OWN VOICE He interrupted himself. The echo rule matches whole tokens against what he just said, which catches his sentence coming back intact and misses it completely when the recogniser mangles it — which over a speaker it always does. A count of new words cannot separate a mis-hear from a real interruption; the proportion can. While his audio is audible, a phrase half made of words he is saying right now is his voice. Cancel words are untouched, and "delete the file" over "Deleting the staging files" still cuts him off. SAYING IT THREE TIMES Told to pass a message to a session he would say "Will say that to it." — tool — "Saying this now." — tool — "Passed that to chitauri, sir." Three sentences for one instruction, because everything he writes is spoken the instant he writes it. The persona forbade it; he complied for a while and then did not. So a turn that uses a tool now says exactly one thing, at the end, and everything written before the last tool call is dropped as narration of something not yet done. Ordinary conversation still streams. A MEMORY THAT COULD NOT BE SAVED A memory writer is refused while anything foreign sits in the generation composing it — correctly, since what it writes becomes trusted text later. The refusal told the user to say it again "in a fresh conversation", and there was no way to start one: no command, no tool, nothing in the persona. Live, that cost four turns and the fact went unsaved. `start fresh` (and the phrases a person actually says) now discards the generation, with no handover, because carrying a summary across would carry the tainted text with it. Hard to trigger by accident: "start the build" and "tell the session to start" are tested not to. STOPPING HIM There was no way to interrupt him that was not a spoken word, and a spoken interrupt is exactly what the echo machinery makes unreliable. Escape, and a button shown only while he speaks, now stop him. A keystroke cannot be misheard. AND SO IT CAN BE SEEN NEXT TIME None of the above was visible. A barge-in, a resume, a dropped narration and the recogniser's whole lifecycle produced no log line at all, so "he repeated himself" and "he could not hear me" could not be told apart from "he was not asked". They all say so now, in the server log, next to the transcripts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Six fixes from a day of using JARVIS by voice on a room microphone rather than a headset. Every one was found in a log, and each ships with a test named for the failure it protects against.
The microphone could go deaf and say nothing about it
Chrome ends a
continuousrecognition session on its own, andsafeStartswallowed theInvalidStateErrorthrown when the engine is between states — by which pointonendhas already fired, so nothing retried and it never listened again.Worse, a session can report
listening, attach audio, and simply return nothing. Measured live: 45 seconds of a live capture with no results, during which nothing anywhere indicated a problem.createMicMonitornow holds its owngetUserMediastream open for the life of the page — no session, no timeout, no restarts — and measures the signal directly. Sound going in with nothing coming out is a named failure that rebuilds the recogniser within three seconds. A small meter in the corner answers "is it even hearing me?" without a log or a console.What the engine is actually doing is tracked (
onstart/onaudiostart) rather than assumed, and a watchdog covers the two shapes no event can report: nothing listening, and listening with no audio.The recogniser is also rotated ahead of Chrome's ~8s
no-speechteardown — but only in silence. Chrome permits one live recogniser, so starting a second aborts the first, and an aborted session never emits the final result for whatever was being said. Measured: interims for a whole sentence arrived, rotation fired 17ms after the last one, and nothing reached the server.He interrupted himself
The echo rule matches whole tokens against what he just said. That catches his sentence coming back intact and misses it entirely when the recogniser mangles it — which, over a speaker, it always does.
A count of new words cannot separate a mis-hear from a real interruption; both clear two. The proportion can. While his audio is audible, a phrase half composed of words he is saying right now is his voice, not the user's. Cancel words are untouched, and "delete the file" over "Deleting the staging files" still cuts him off.
He said the same thing three times
Told to pass a message to a session, he would say "Will say that to it." — tool — "Saying this now." — tool — "Passed that to chitauri, sir." Three sentences for one instruction, because everything he writes is spoken the instant he writes it.
The persona already forbade it; he complied for a while and then didn't. So a turn that uses a tool now says exactly one thing, at the end, and everything written before the last tool call is dropped as narration of something not yet done. Ordinary conversation still streams as before.
A memory that could not be saved
A memory writer is refused while anything foreign sits in the generation composing it — correctly, since what it writes becomes trusted text later. The refusal told the user to say it again "in a fresh conversation", and there was no way to start one: no command, no tool, nothing in the persona. Live, that cost four turns and the fact went unsaved.
start fresh(and the other phrasings a person actually uses) now discards the generation, with no handover — carrying a summary across would carry the tainted text with it. It is tested to be hard to trigger by accident: "start the build" and "tell the session to start" do nothing.There was no way to stop him
Every interrupt was a spoken word, and a spoken interrupt is exactly what the echo machinery makes unreliable. Escape, and a button shown only while he speaks, now stop him. A keystroke cannot be misheard.
hushis deliberately not inCANCEL_WORDS, and there is a test asserting it stays out.And so it can be seen next time
None of the above was visible. A barge-in, a resume, a dropped narration, and the recogniser's entire lifecycle produced no log line at all — so "he repeated himself" and "he could not hear me" were indistinguishable from "he was not asked". They all say so now, in the server log, next to the transcripts.
Testing: 2425 pass,
tsc --noEmitclean,npm run buildclean. All changed Python files parse under 3.12 (CI's version).🤖 Generated with Claude Code