TacidOS
Developers
Documentation

Decision 0075

0075. A microphone is a principal anybody in the room can be

Architecture decision record 0075: 0075. A microphone is a principal anybody in the room can be

Status
accepted
Date
2026-09-07

Status: accepted. Builds the pipeline ADR-0024 settled and PLAN.md §3.14 specified, and says plainly which half is not built.

Context

Voice was one of the four things Phase 2 promised and the only one nothing had been written for. What existed was the shape around it: whisper.cpp is an engine the registry names and the supervisor appeared to know how to start — appeared, because reading whisper.cpp v1.9.2 later showed it binds a port and never a socket, which took a door the supervisor holds to make possible (ADR-0086) — and Provenance::Voice is a principal the policy caps and the injection corpus exercises. There was no capture, no partial, no speech coming back, and no key bound to any of it.

The reason to build it carefully is not accuracy. Every hard thing about voice on a machine that can change itself is about who is speaking and when it is listening:

  • A microphone is a principal anybody in the room can be. A guest, a television, a phone call on speaker.
  • A transcript is untrusted text that arrived by a route with no provenance beyond “somebody spoke”, and no confidence score changes that.
  • A machine that listens when nobody asked it to is a machine nobody should own — and the failure is not that it heard something, it is that it said it was not listening.

Decision

The pipeline is a bounded, interruptible state machine whose indicator is its state, and the hardware is behind two traits.

Four rules, each of which exists because of what happens when it is missing.

The microphone is open exactly while a key is held, and Listening::Held is both the fact and what the indicator renders. Not a state plus a flag: a second value carrying the same fact is a second value to keep true, and the first time it disagrees the machine is listening while claiming not to. There is no method here that opens capture without moving to Held.

One utterance is bounded at thirty seconds. A key that sticks, a shell that misses a release, a person who walks away — without a ceiling, any of those is an open microphone until the machine is rebooted. That is ADR-0062’s lesson in the one place where an unbounded wait is also a privacy failure. What was captured is still transcribed, and Heard::cut_short says it was cut off — a machine that acts on half a sentence without saying so is worse than one that asks.

Speech yields to speech, both ways. A person who starts talking while the machine is talking has interrupted it, and playback stops before capture starts; a machine that finished its sentence first is one people learn to talk over and then wait for anyway. And the machine will not begin speaking while a key is held, because talking over somebody holding a button is how a person learns not to hold it.

Interrupting can never be refused. It takes no arguments, returns nothing, and works in every state. The thing a person reaches for when a machine is misbehaving must not itself be refusable.

The four budgets are measured, not asserted. PLAN.md §3.14 publishes release-to-first-partial ≤ 400 ms, an eight-second utterance to final text ≤ 1.5 s, first token ≤ 300 ms and first audio ≤ 400 ms. They are constants a turn stamps itself against, and what comes back says what each stage took and whether it was inside its ceiling — on this machine, with this model, today. A stage nobody entered is absent rather than zero, because reporting zero milliseconds would be a machine claiming to have beaten a budget it never entered. Missing a budget is a fact about a machine, not an error.

tacid-voice is agent-mutable, on the same reasoning as tacid-infer: what it produces is untrusted text, and a rewritten push-to-talk cannot make the machine act on what it hears, because nothing spoken has ever been trusted. What it could do is listen when it should not — which is exactly why the indicator is the state rather than a flag beside it, and why that is the one invariant this crate holds on its own.

Consequences

  • Both of the first two consequences here have since been overtaken, and the sentences they replace are worth keeping visible. They read: “Nothing here has captured a microphone” and “No surface binds a key to this yet”. Both were true when this was written and neither is now, and the reason to say so rather than to quietly rewrite them is that a document nobody can tell has changed is a document nobody can trust about the parts that have not.
  • Capture and Playback have implementations that open a device: Microphone and Speakers, over ALSA, in tacid-voice’s device module. checks.*.voice boots a machine with snd-aloop — a loopback card, so that what is played is what is captured and a test can assert the bytes rather than the absence of an error — and watches the microphone open exactly while a key is held, close when it comes up, close on its own when the key never does, and hand back what was played into it. The budgets are still unmeasured on real hardware, which is the half of the original sentence that stands.
  • A surface binds a key: Super and the backtick, in the Hyprland session, as a bind on the way down and a bindr on the way up — which is what makes it push-to-talk rather than a toggle, and what checks.*.desktop asks the running compositor rather than the file.
  • A turn still ends in a loudness and not in words. tacid-session’s voice module reports the RMS of what it heard, because no machine this tree builds can fetch a transcription model: the registry names whisper.cpp, the supervisor can start it through the door it holds in front of the port it binds, and every weight in the registry lacks a verified hash and a file to fetch. That is one missing artefact and not a missing design, and it is the last thing between a person holding a key and a machine that answers.
  • The wake word is not built and is not started here. It is opt-in in ADR-0024, it needs a model and a persistent indicator, and it is the one path where audio is captured without a key being held — so it wants its own record rather than an inheritance from this one.
  • The transcript’s route into the daemon is the ordinary one: an ask carrying Provenance::Voice, which the injection corpus already exercises. Nothing new was needed there, which is the sign that the earlier work was right.

Alternatives rejected

Voice activity detection instead of push-to-talk. The machine decides when you meant to talk to it. Every failure mode of that is “it was listening and you did not know”, which is the one failure this design refuses. It stays available as an opt-in later, with the same indicator rule.

An indicator flag beside the state. The obvious shape, and it means the machine has two answers to “are you listening”. One of them will be wrong eventually, and it will be the one on the screen.

Let the machine finish its sentence before listening. Simpler, and it teaches people to wait for a machine that will not stop for them — which is how a voice interface stops being used.

Trust a familiar voice. Speaker recognition would make voice a stronger principal, and it would be a confidence score standing where an authorisation belongs. The model is not an authorization authority; neither is a voiceprint.