OpenFlow

Seven states, and nothing else may write to them.

Dictation fans out across the bubble, the microphone, the provider, the refiner and the inserter. Left alone that becomes scattered booleans which admit impossible combinations — so one component owns the state.

IDLE PREPARING RECORDING TRANSCRIBING REFINING INSERTING DONE any state CANCELLED ERROR_RECOVERABLE ERROR_FATAL audio discarded, nothing persisted retry or copy, transcript kept next start re-enters preparing

From docs/adr/0002-dictation-state-machine.md

one transition per pair
Every (state, event) pair has exactly one defined transition, or is explicitly ignored. A transition-table test covers it.
inserting is not cancellable
Insertion either lands or fails, and the bubble shows which. You cannot back out halfway and leave a field in pieces.
cancel discards everything
Cancel during recording, transcribing or refining stops capture, cancels the provider flow, drops the audio, and persists no transcript.
every step times out
A suspending step that hangs becomes ErrorRecoverable(Timeout) rather than a dictation that never ends.
cleanup never eats your words
A refiner failure degrades to the raw transcript, marked unprocessed. Losing what someone said to a punctuation bug is the worst outcome available.

Eight deterministic stages.

No language model. Every stage is a rule you could implement on a Tuesday, which is the point: the same transcript cleans the same way every time, and you can read exactly what changed.

  1. NormalizationWhitespace, casing baseline.
  2. Spoken-command expansion“full stop” becomes a period; new line, new paragraph, quotes, parentheses.
  3. Backtracking resolution“scratch that…” deletes back to the last sentence boundary. No, I mean and actually are soft markers only — in V1 the next clause does not replace the last one.
  4. Filler removal“um”, “uh”, “you know”, “like”. Per-language lists, user toggle.
  5. List detection“number one”, “first” become a numbered list; a line-leading “dash” becomes a bulleted one.
  6. DictionaryYour wrong→right map. A trigger matches the words you say — case and punctuation don’t decide it, so sig never fires inside signal.
  7. SnippetsYour shorthand→expansion map, matched the same way, user-toggled.
  8. App-style formattingOne of Casual, Neutral or Formal — capitalisation and ending punctuation, assigned per app.
worked example the demo in the hero, annotated
raw      i'm sorry scratch that friday works too. um like thursday at ten am

stage 3  scratch that friday works too.   false start deleted
stage 4  um like                        fillers removed
stage 8  thursday at 10am               → Neutral preset applied

result   Thursday at 10am.

Style runs last so it sees the final content. A refiner exception anywhere in the pipeline degrades to the raw transcript marked unprocessed — it never fails the dictation.

From docs/architecture/refiner.md

Five rules, in a fixed order.

The order is part of the contract, because “which rule excluded this provider” is the explanation you are owed. Consent comes before capability; capability before cost; health last, because it is the most volatile and least authoritative signal.

1 · manual preference
A ranking override among eligible providers. Never a way over a hard exclusion.
2 · privacy and offline
local-only excludes cloud. offline excludes online. These are your instructions, not the router’s judgement, so they come first.
3 · capability match
Does this provider actually support the language you asked for, and work without a network if you said so.
4 · cost ceiling
Compared against the worst case — rate × maximum audio duration — not the amount it turns out to be. A ceiling you can exceed after the router commits is not a ceiling.
5 · health
UNAVAILABLE and MODEL_MISSING skip. DEGRADED is eligible but ranked last. Health never empties the candidate set: if skipping would leave nothing, degraded providers are admitted and the decision is recorded as DEGRADED_LAST_RESORT.

There is no user-facing cost slider in V1. With no billable provider, a ceiling would control nothing — the field exists so the first cloud adapter is a drop-in rather than a rewrite. Automatic routing by measurement is explicitly out of V1; when it arrives it reads the same decision log.

From docs/adr/0004-adaptive-dictation-router.md

Getting the text in.

Writing into a field that belongs to another app is the part that can actually break, so it degrades in three steps and re-resolves its target rather than trusting a stale handle.

  1. SET_TEXTSet the field’s contents directly. Fast, and it replaces a selection rather than inserting into it.
  2. PASTEWrite to the clipboard and paste, which preserves a caret inside existing text.
  3. Copy and promptPut it on the clipboard, tell you it is there, and let you paste. The clipboard is preserved beforehand and restored afterwards.

Sensitive fields are refused before the microphone opens. If the focused field turns out to be sensitive partway through, the audio and the transcript are discarded and nothing is written. And if you cancel, there is nothing to find later — no transcript, no audio, no record that a dictation ever started.

From docs/adr/0003-safe-text-insertion.md

Read the decisions

The decisions that shaped it, and the documents behind them, in one index.

Documentation index

Read the privacy rules

What leaves the device, what is stored, and what it refuses to do.

Privacy