Add heartbeat: reminders and delivered files reach Bolt Assistant
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>
This commit is contained in:
co-authored by
Claude Code
parent
a8c7db9520
commit
751d1c8b69
@@ -411,3 +411,55 @@ def test_transcription_falls_back_to_one_upload():
|
||||
t.add([frame(LOUD)]) # nowhere to go: the live socket never opened
|
||||
assert t.finish(frame(LOUD) * 2) == "hey bolt"
|
||||
assert b"".join(late.audio) == frame(LOUD) * 2
|
||||
|
||||
|
||||
# ── heartbeat announcements ──────────────────────────────────────────────────
|
||||
|
||||
def test_announcements_are_spoken_when_idle():
|
||||
a, asked, played, logs, _ = make([])
|
||||
a.announce("You have a reminder.")
|
||||
a._drain_announcements()
|
||||
assert "You have a reminder." in spoken(played)
|
||||
assert a.state == "idle"
|
||||
|
||||
|
||||
def test_announcements_wait_for_idle():
|
||||
a, asked, played, logs, _ = make(["It's three o'clock."])
|
||||
a.state = "busy"
|
||||
a.announce("You have a reminder.")
|
||||
a._drain_announcements()
|
||||
assert spoken(played) == [] # held back while Bolt is talking
|
||||
a.state = "idle"
|
||||
a._drain_announcements()
|
||||
assert "You have a reminder." in spoken(played)
|
||||
|
||||
|
||||
def test_announcements_hold_while_a_transcript_is_in_flight():
|
||||
a, asked, played, logs, _ = make([])
|
||||
a._pending.append(object()) # an utterance is still at the server
|
||||
a.announce("You have a reminder.")
|
||||
a._drain_announcements()
|
||||
assert spoken(played) == []
|
||||
a._pending.clear()
|
||||
a._drain_announcements()
|
||||
assert "You have a reminder." in spoken(played)
|
||||
|
||||
|
||||
def test_announcements_hold_while_someone_is_speaking():
|
||||
a, asked, played, logs, _ = make([])
|
||||
FakeTranscription.script.append("") # the start event still mints a transcriber
|
||||
for f in [frame(LOUD)] * 3:
|
||||
a.process(f) # the segmenter is mid-utterance
|
||||
a.announce("You have a reminder.")
|
||||
a._drain_announcements()
|
||||
assert spoken(played) == []
|
||||
a.segmenter.reset()
|
||||
a._drain_announcements()
|
||||
assert "You have a reminder." in spoken(played)
|
||||
|
||||
|
||||
def test_blank_announcements_are_dropped():
|
||||
a, asked, played, logs, _ = make([])
|
||||
a.announce(" ")
|
||||
a._drain_announcements()
|
||||
assert spoken(played) == []
|
||||
|
||||
Reference in New Issue
Block a user