# v2.35.0 — the update check could not report an update

**Released 2026-08-27.** Upgrade:

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

Two fixes in one function's blast radius, both reported or found by running the shipped
product rather than by any test. Between them, **neither bundled platform could tell you
that a new version existed** — Windows said nothing at all, and macOS said something
confidently wrong.

## Windows: "Check for updates…" never showed an update

Reported from a Windows desktop: the tray's **Check for updates…** never announced a new
version and never updated anything, so upgrading meant uninstalling and installing the new
build by hand.

Windows gives a balloon body a 256-wide-character buffer (`NOTIFYICONDATA.szInfo`) and
**discards an oversized balloon whole** — no truncation, no exception, nothing in a log.
Measured against the real messages:

| Case | Body length | Result |
|---|---:|---|
| Windows installer, update available | 512 | dropped |
| check failed / offline | 623 | dropped |
| up to date | 40 | shown |
| Scoop / Chocolatey / winget, update available | 60–79 | shown |

The two cases that carry information were exactly the two that vanished. The only message
able to render was *"YazSes is up to date"*, which is why the entry looked like it could
never find anything.

This had already been found and fixed for **About**, six lines above in the same file, and
the update path was not covered. The fitting now happens inside the one function that
reaches the Windows notification API, so every balloon — including the daemon's relayed
self-healing messages, which are the longest the tray shows — inherits it, and a structural
test fails the build if any call bypasses it. A test pinning one path would have passed on
the other bug, which is how this shipped twice.

**And the click now does something.** A Windows-installer install has no upgrade command by
design — the upgrade is a downloaded `.exe` — so the tray's whole response was to print the
releases URL into a balloon, where it is text rather than a link. It now opens the download
page, and says the installer upgrades in place and keeps your settings and models, which is
what makes the uninstall-first workaround unnecessary.

Downloading and running the installer automatically is deliberately *not* done: that is
executing a fetched binary on your behalf, and it waits on code signing.

## macOS: the `.app` was told to download a Windows `.exe`

Found next to the balloon bug, in the same function. `detect_install_method` classified
**anything** with `sys.frozen` set as `windows-installer`, because the frozen branch was
written when Windows was the only bundled build. The `.dmg` `.app` is frozen too, so every
Mac user who opened **Check for updates…** was told, in confident step-by-step detail, to
download a `YazSes-<version>-windows-<arch>.exe` — a file that cannot run on their machine,
in a message with nothing to suggest the advice itself was the problem.

A wrong instruction stated with certainty is worse than a silent failure: the reader has no
reason to doubt it.

A bundle is not a Windows bundle; it is a bundle on whichever OS is running it. There is now
a `macos-app` method with:

- its own steps — open the current `.dmg`, drag YazSes to Applications, replacing the old
  copy;
- its own recovery hint on the *[update did nothing](../how-to/update-did-nothing.md)* page,
  which gained the macOS tab it never had;
- Homebrew named as the alternative (`brew upgrade --cask yazses`) rather than *guessed at* —
  a cask install and a direct `.dmg` install are the same bundle and are not distinguishable
  from inside it, and a command that exits 0 having done nothing is the failure this whole
  module keeps being rewritten against;
- the GitHub release as its version source, because the artifact **is** a release asset and
  PyPI carries no `.dmg`.

The macOS tray's **Check for updates…** now opens the download page for the same reason the
Windows one does, and `yazses update --check` on a `.app` stops exiting 1 with *"no automatic
upgrade is available"* — the method is recognised, so it prints real steps and exits 0.

## Also

The balloon **title** is now bounded too. `NOTIFYICONDATA.szInfoTitle` is a 64-wide-character
buffer, the same family of fixed buffer as the body. No title YazSes ships comes close — the
longest is 47 characters — so this is the class being closed rather than a live overrun, and
the same structural test now covers both fields of every `notify()` call in the Windows tray.

## Nothing else changed

Dictation, the models, your config and the learning corpus are untouched. Everything above
is in the update path and the tray.
