Skip to content

Fix the voice loop: a deaf microphone, his own voice interrupting him, and everything said three times - #32

Merged
ethanplusai merged 1 commit into
mainfrom
fix/self-echo-barge-in-and-needless-confirmation
Sep 6, 2026
Merged

ethanplusai merged 1 commit into
mainfrom
fix/self-echo-barge-in-and-needless-confirmation

Conversation

@ethanplusai

Copy link
Copy Markdown
Owner

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 continuous recognition session on its own, and safeStart swallowed the InvalidStateError thrown when the engine is between states — by which point 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: 45 seconds of a live capture with no results, during which nothing anywhere indicated a problem.

createMicMonitor now holds its own getUserMedia stream 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-speech teardown — 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. hush is deliberately not in CANCEL_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 --noEmit clean, npm run build clean. All changed Python files parse under 3.12 (CI's version).

🤖 Generated with Claude Code

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>
@ethanplusai
ethanplusai merged commit fd135d3 into main Sep 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant