The client never called /desk/report_status, so Bolt's reminders and any
files he delivered to this session sat in the server's outbox forever -
the same dead-air failure the pet client showed when its heartbeat stopped.
Now a heartbeat thread (HEARTBEAT_SECONDS, default 60; 0 disables) reports
in, speaks announcements through the assistant's queue when the mic is
idle, and downloads the outbox to BOLT_DOWNLOAD_DIR (default
~/Downloads/Bolt) via .part-then-rename, never overwriting. A startup
health probe warns loudly when the server refuses the DESK_API_KEY.
Also: add the .env.example the README's setup step references (it was
missing, so `cp .env.example .env` failed for everyone), and gitignore
.claude/ so Claude Code's worktree directory stays out of the repo.
Tests: tests/test_heartbeat.py (12) plus announcement tests in
tests/test_assistant.py (6); full suite 135 passed.
Co-Authored-By: Claude Code <noreply@anthropic.com>
stt.finish() used to run inline in the frame loop, deafening the
assistant for up to ~4s per utterance while the server returned the
transcript (and whatever was said meanwhile was later drained). The
finish now runs on a background thread per utterance; the segmenter
keeps consuming mic frames, so speech during the wait becomes its own
utterance and is transcribed too. Transcripts are delivered oldest-
first between frames, deferred while an utterance is in progress, and
the command/follow-up window no longer times out on transcription
latency. Speaking stays half-duplex (echo protection).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>