Skip to content
C.W.K.
Stream
Lesson 04 of 04 · published

A Loop That Stops Says Why

~12 min · observability, ux, errors, feedback

Level 0Muted
0 XP0/35 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"It just says Listening with one red light... there's no dynamic display at all, so I can't tell whether it's working." — Dad, 2026-09-26

Is It Even Listening?

A hands-free loop has almost no visible surface. The face, a ring, a word. At first the listening state was a single red dot, and Dad pointed out the obvious problem: with nothing moving, he couldn't tell whether his voice was being heard at all. Then, a moment later: not red, please, teal; red looks like a warning light. Now, while listening, the ring around the face and a seven-bar meter under it move with his voice, in teal. The level is the audio's RMS mapped onto a logarithmic scale, so a quiet room sits at zero and ordinary speech fills most of the meter. While the soul thinks, the ring turns grey with three dots; while she speaks, it glows in her accent color. Every phase looks different from every other.

The web voice screen while listening: Pippa's face inside a teal ring, a seven-bar teal meter under it, the word 'Listening' and a Pause button; along the top, an attach button, toggles for On Air, Cut in and Captions, and the KO language.
Listening on the web: the teal ring and the seven-bar meter move with Dad's voice.

The Error That Closed Its Own Screen

The same day Dad reported that the sidekick's loop "turned itself off" right after he pressed KO. Investigating it found something worse than the failure: the reason was being written, but onto the face screen, and the failure's own handling closed that screen. The message existed for a fraction of a second on a view nobody could see.

So both clients gained a stopped phase. A refusal, a socket that closes, or a quiet minute now keeps the face up with the reason on it, and a tap listens again; only the close button ends the talk. A socket closed before it ever opened used to leave the phase stuck at connecting; now it stops with the close code and reason, the same as any other close. The cause of Dad's original case was never reproduced: two presses in the test browser against the real transcriber worked, and the engine logged both sessions as granted. That's fine. The point of the fix is that the next time it happens, the screen will say why.

Two More Silent Failures, Made to Speak

The same pattern fixed two others. The web's dictation start kept hold of a recorder that had failed to start, so every later press returned early and the microphones stayed dead until a reload; a failed start now releases the microphone and says so. And on the phone, a reply that was deliberately passed over now says why beside the Voice chip (voice was off, another conversation was open, Dad was already talking), and a reading that fails keeps its reason instead of clearing it.

The Rule Underneath

Every one of these was a state the code knew and the user didn't. A voice interface has no scroll-back, no console and no error page. If the loop knows why it stopped, the face has to say it, in words, and stay up long enough to be read.

Code

A live level meter, and stop reasons that stay on screen·typescript
// Show that it's listening, and when it stops, keep the reason on screen.

/** A 16-bit RMS mapped to 0...1 on a log scale: a quiet room (~120) sits
 *  at 0 and ordinary speech (a few thousand) fills most of the range. */
export function levelFromRms(rms: number): number {
  if (!(rms > 0)) return 0;
  return Math.max(0, Math.min(1, Math.log10(Math.max(rms, 1) / 120) / Math.log10(40)));
}

/** Seven teal bars under the face: how much of the meter this level lights. */
export function meter(level: number, bars = 7): string {
  const lit = Math.round(level * bars);
  return '▮'.repeat(lit) + '▯'.repeat(bars - lit);
}

type Stop =
  | { cause: 'refused'; detail: string }
  | { cause: 'socket-closed'; code: number; reason: string; opened: boolean }
  | { cause: 'quiet-minute' }
  | { cause: 'mic-failed'; detail: string };

/** The face screen stays up in 'stopped' and shows this; a tap listens again. */
export function stopMessage(stop: Stop): string {
  switch (stop.cause) {
    case 'refused':
      return `Listening was refused: ${stop.detail}`;
    case 'socket-closed':
      return `Listening closed ${stop.opened ? '' : 'before it opened '}(${stop.code}${stop.reason ? `: ${stop.reason}` : ''})`;
    case 'quiet-minute':
      return 'Nothing heard for a minute, so listening stopped.';
    case 'mic-failed':
      return `The microphone didn't start and was released: ${stop.detail}`;
  }
}

for (const rms of [0, 120, 400, 1500, 4800, 12000]) {
  console.log(String(rms).padStart(6), meter(levelFromRms(rms)), levelFromRms(rms).toFixed(2));
}
console.log(stopMessage({ cause: 'socket-closed', code: 1008, reason: 'token expired', opened: false }));
console.log(stopMessage({ cause: 'quiet-minute' }));
console.log(stopMessage({ cause: 'mic-failed', detail: 'NotAllowedError' }));

External links

Exercise

Run the code and look at how RMS values map to the meter. Pick three real situations (a quiet room, speaking softly, speaking normally) and measure their RMS from your own microphone, then decide whether the 120 floor and 40x range suit your room. Finally, list every way your own voice feature can stop on its own and write the one-line message each should show.
Hint
Measure before you tune: a floor chosen for one room makes a noisy room look like constant speech and a quiet one look dead. For the stop list, include the ones that feel impossible (a socket that closes before it opens, a microphone that fails to start); those are the ones that will otherwise fail silently.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.