# v2.38.0 — the failures that never told anyone

**Released 2026-09-24.** Upgrade:

```sh
sudo snap refresh yazses     # snap
pipx upgrade yazses          # or: uv tool upgrade yazses
scoop update yazses          # Windows
```

One theme, found by auditing every path that hits an error or needs a permission against
a single rule: **say so, with a reason the user can act on, somewhere they will actually
see it.** Four defects fell out. Two were shipping.

## Every permission denial was reported as a microphone problem

`mic-permission`'s marker was the bare word `"permission"`, and the classifier folds an
exception's **class name** into the text it matches. Every `PermissionError` therefore
matched that rule first — and a `/dev/uinput` denial, a `/dev/input/event*` denial and a
refused portal consent are all `PermissionError`s.

Reproduced against the real classifier before anything was changed:

| what actually failed | what the user was told |
|---|---|
| `/dev/uinput` denied (typing) | "YazSes is not allowed to use the **microphone**" |
| `/dev/input/event3` denied (hotkey) | "…use the **microphone**" |
| portal consent refused | "…use the **microphone**" |

So someone who declined the Wayland typing prompt was sent to the *audio* privacy pane to
fix a *typing* fault — advice that cannot work. `inject-permission` sat below it and was
unreachable dead code for the errors it was written for.

Seven permission families now name themselves, each with the command that actually fixes
it: refused portal consent, `/dev/uinput`, `/dev/input/event*`, the snap's `audio-record`
and `raw-input` interfaces, and macOS Accessibility and Input Monitoring. The microphone
rule now has to carry audio evidence of its own, and an unrelated `PermissionError` falls
to the generic diagnosis — which is what earns the **Prepare a bug report** button, the
right answer when YazSes genuinely does not know.

## Spoken Edit and Punch-In erased your text and then lost it in silence

Both erase the previous dictation and retype it. Spoken Edit wrapped the pair in a `try`
whose handler was a `log.debug`, so when the retype failed the erase had **already
landed**: the sentence disappeared, nothing replaced it, and the only record was a debug
line nobody reads. Punch-In had no `try` at all — a CLI caller saw the exception, but a
correction triggered by voice or from the tray lost the text with nobody to tell.

Both now report through the path that reaches the person holding the key. There is
deliberately **no automatic restore**: a failed injection may have typed part of the
replacement, and retyping the original on top of a partial write corrupts it further.
Spoken Edit also no longer records the replacement in its ledger — writing text there that
never reached the screen would make "scratch that" delete characters that are not on it.

## No permission was checked at startup at all

Every probe and every remedy string in `platform/*/permissions.py` already existed. Their
only consumer was `yazses doctor`, a command you have to know to run. So the daemon would
start with keyboard access denied, reach IDLE, paint the tray healthy, and let you hold
the key into a void — which is, word for word, what the first person to run the macOS
build reported on [#182](https://github.com/MSKazemi/yazses/issues/182): hotkey pressed,
"nothing happens", no prompt, no explanation.

Missing grants are now announced on the desktop at startup, carrying the platform's own
remedy text. **Only DENIED speaks.** UNKNOWN stays silent, because macOS reports a
microphone nobody has been asked about yet as `NotDetermined`, and an absent PyObjC
reports UNKNOWN too — reddening a working install on the strength of a probe that never
ran is a mistake this project has already made once. A probe that *raises* is treated the
same way.

## The Wayland consent dialog now explains itself before it opens

`RemoteDesktop` is the only Wayland API for synthetic input, so the portal backend added
in v2.37.0 has to use it — but GNOME and KDE title the dialog **"Remote Desktop"** and
confirm it with a button marked **"Share"**. Read cold by someone who installed YazSes
*because* nothing leaves their machine, that says the opposite of what is happening, and
Cancel is the rational answer.

YazSes now speaks first: what is actually requested is the keyboard alone — no screen
capture, no mouse, nothing sent anywhere, approve once and it is remembered. Every clause
is checked against what the client requests rather than asserted. `yazses start` also
names the consequence of skipping `yazses setup`, so the terminal pre-empts the surprise
instead of racing it. If a restore token already exists no dialog is coming, and nothing
is said.

## Also fixed

- **A first run that downloads a model says so.** The speech model is several hundred
  megabytes and was fetched with a `log.info` and nothing else, so a GUI install looked
  like an app that started and then did nothing for minutes. A blocked model download was
  [#310](https://github.com/MSKazemi/yazses/issues/310), the first bug reported by a real
  user. Silent on every later run, and in offline mode.
- **A crash loop no longer turns one problem into five toasts.** The repeat-silence record
  lived in memory, so it could only damp repeats inside one process — while
  `Restart=on-failure` with `StartLimitBurst=5` produces five fresh processes a minute. It
  now survives restarts, written atomically because the daemon is killed at arbitrary
  moments by that very loop.
- **Failures printed to a terminal nobody was watching now reach the desktop.** This is
  the mechanism behind an enabled engine silently disappearing on the next
  `uv tool upgrade`: the install succeeds, the caller is told it worked, and the loss
  surfaces much later as dictation quietly using a different engine.
- **The bug-report offer is a decision, not a naming accident.** It was derived from a slug
  prefix, which meant a future recognised-but-unfixable fault would inherit "no button"
  because nobody was asked the question.

## Verification

Both shipped defects were reproduced against the real code before being fixed, and each
fix was mutation-tested — the change was reverted to confirm its test goes red rather than
passing vacuously. Gates at release: **ruff clean**, **mypy clean across 525 source
files**, **15,407 tests passed / 300 skipped**.

The macOS permission announcements are covered by unit tests against fake backends and by
the macOS CI leg; they have not been confirmed by a human on a physical Mac. A field report
on [#182](https://github.com/MSKazemi/yazses/issues/182) remains the most useful thing
anyone with a Mac can contribute.
