- daemon: GLib.timeout_add (thinking animation) was called from a background
thread — NOT thread-safe in PyGObject/GTK3 and the prime suspect for the
session-level freezes. Fixed via idle_add so timeout_add runs on main thread.
- daemon: add SIGQUIT handler — kill -QUIT <pid> dumps all thread stacks to
the log so the freeze location can be identified without rebooting.
- daemon: VAD hard-timeout safety net — if LevelMeter fails to open the mic
(device busy because wakeword pw-record holds it), dictation now auto-stops
after 30s instead of hanging forever. Wakeword sessions also capped at 60s.
- overlay: coalesce set_level() calls the same way set_text() is coalesced,
so mic level spikes from 3 simultaneous pw-record streams can't flood the
GTK main loop either.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>