Skip to content

Release candidate: server-v0.2.0-rc.1 - #185

Open
BrettKinny wants to merge 156 commits into
mainfrom
release/server-v0.2.0-rc.1
Open

BrettKinny wants to merge 156 commits into
mainfrom
release/server-v0.2.0-rc.1

Conversation

@BrettKinny

@BrettKinny BrettKinny commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Collects everything intended for server-v0.2.0-rc.1 on one branch so the Docker host runs a single commit.

Contents

Deferred

Verification

  • Tests:
    • pytest tests/ custom-providers/pi_voice/tests/: 802 passed (at 5651a59).
    • pytest dotty-behaviour/tests/: 316 passed.
    • ruff check .: clean.
  • Code review (2026-10-21): four slices. Voice and Kid Mode safety, and dashboard and behaviour, went through /code-review ultra; the test harness and docs went to subagents. Every finding was checked against the branch, and every real one was fixed with a test (9ab676b to e960351).
  • Reference host: runs d80b5e1 (xiaozhi; dashboard and dotty-behaviour at e960351). The robot reconnected with 15 tools, with no component errors.
  • Physical robot A/V run: three consecutive passes per case, W01 ten.
    • Runs: 2026-10-21 and 2026-10-22 morning, then the 2026-10-23 night on the builds above, with fixes deployed between cases.
    • Complete (22):
      • Conversation: V01, V03, V04, V05, V06, V07.
      • Tools: T01 memory_lookup, T02 remember, T03 recall_person, T04 remember_person, T05 think_hard, T06 take_photo (the Kid Mode refusal), T07 play_song.
      • Kid Mode: K01 safe joke, K03 nonsense → "say that again", K04 a dangerous sentence is never repeated.
      • States: S02 story, S03 security, S06 dance, S04 sleep by voice plus dashboard wake, S05 sleep ignores voice plus dashboard wake.
      • Wake: W01 cold wake word, ten in a row.
    • Unresolved, needs a maintainer decision:
      • V02 (reference mic): the robot said all five words every time.
      • K02: "What is 12 plus 10 room?" gets "22" two times in three. Dotty never repeats the nonsense, but doesn't ask again, and tightening the rule didn't help (reverted).
    • Remaining voice issues: Kid Mode: asked for a short joke, Dotty sometimes tells a long story #191, a joke sometimes comes back as a short story (also seen once in K01).
  • Firmware findings (not in this PR; issue draft pending):
    • Heap corruption on WebSocket reconnect: three StoreProhibited panics in tlsf_malloc, each about 1 s after the server's idle close and reconnect. The likely cause is that EspTcp's destructor doesn't wait for its receive task after a server-side close. A fix is drafted on BrettKinny/StackChan@agent/fw-tcp-teardown (94def10), not yet built.
    • Audio front-end overload: the AFE feed overflows (500–1,100 a minute) after about an hour, and the CPU 1 task watchdog fires. Hearing degrades until a reset.
    • Abort on a codec I2C read failure at speaker enable (ESP_ERROR_CHECK in CoreS3AudioCodec::EnableOutput), possibly the same supply dip as the power losses.
    • Earlier: two power losses under servo/speaker load (2026-10-20/21) and an idle take_photo hang (Firmware main loop hangs when take_photo arrives before the camera stream has started #189).
  • Not verified: T06 with Kid Mode off (camera on); toggle light colours; head-touch wake without a purr; the leading-emoji face on screen (UAT UC3: leading-emoji face contract unenforced on PiVoiceLLM path — surprise/thinking faces never render #168), which needs a frame review.

Not for unattended merge

Per AI_TRANSPARENCY.md, a human reviews and merges. Squash-merge this PR: its history includes a WIP snapshot (258b9e2) whose stray files were removed in 061fe12, and squashing keeps them out of main. Delete release/server-v0.2.0-rc.1 after merging: its history also has the maintainer's host and robot MAC in 4e5aedb (fixed at the tip in 67c91ff). This PR was assembled by AI agents (Claude Opus 5.5) under the maintainer's direction.

🤖 Generated with Claude Code

BrettKinny and others added 30 commits July 12, 2026 18:52
Correct the pinned firmware source, first-boot provisioning path, effective OTA configuration, and stale compatibility metadata. Add regression guards, protect raw UAT evidence from accidental commits, and keep the calendar route test offline.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Correlate new_session acknowledgements by request ID and surface reset failures instead of accepting stale responses.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
…ions

Replace the broken direct-ALSA capture default with the C920 PipeWire source and validate decoded sample coverage. Add bounded session coordination, honest evidence gates, checkpoint quarantine, and local clip review exports.

Co-Authored-By: GPT-6 <noreply@openai.com>
Add PCM clipping and silence metrics, optional pre-rendered prompts, truthful follow-up readiness, and a source-based feature inventory. Preserve unknown visual and tool execution outcomes.

Co-Authored-By: GPT-6 <noreply@openai.com>
…wups

Keep VAD enabled with peak-limited analysis gain and immutable provenance. Require actual wake frames for cold-wake claims; block warm tests unless the live microphone is listening. Use deployed follow-up readiness contracts.

Co-Authored-By: GPT-6 <noreply@openai.com>
Gate ambient turns on current motion ownership and refresh the post-chat quiet interval. Eight deterministic failures reproduced before the fix; 44 focused tests pass. Physical acceptance remains pending.

Co-Authored-By: GPT-6 <noreply@openai.com>
Microphone evidence proved VAD can miss genuine quiet robot replies. Keep original case evidence and do not infer silence from an empty transcript. 57 coordinator regressions pass.

Co-Authored-By: GPT-6 <noreply@openai.com>
Discover all test suites including cached take_photo, execute previously skipped memory integration checks against invented SQLite fixtures, and clear live service test overrides. Eleven suites and 124 assertions pass without household data.

Co-Authored-By: GPT-6 <noreply@openai.com>
Preserve per-attempt prompt provenance and reject stale or mismatched waveforms before playback. Clear inherited overrides so one case cannot accidentally play another prompt. 75 coordinator regressions pass.

Co-Authored-By: GPT-6 <noreply@openai.com>
Keep default threshold 0.5 and prompt transcription unchanged. Validate response-only prospective settings and persist actual transcription provenance; preserve inconclusive silence and unverified text semantics.

Co-Authored-By: GPT-6 <noreply@openai.com>
Content-only and speaker-without-language envelopes were treated as spoken JSON. Decode content independently of optional metadata and reject non-text payloads before noise filtering. Regression tests exercise startToChat with mocked side effects; seven pre-fix scenarios failed and now pass. Live deployment remains pending.

Co-Authored-By: GPT-6 <noreply@openai.com>
Document touch/admin wake, disabled physical face and voice producers, neutral parking, and historical release-pin differences. Correct the current voice-tool catalogue to seven without changing product behavior.

Co-Authored-By: GPT-6 <noreply@openai.com>
Physical V06 correctly recognized a small-robot joke, then the sliding edit-distance correction manufactured a story command. Limit known-phrase cleanup to exact word sequences with punctuation normalization, preserving sentence boundaries and explicit observed name aliases. Actual startToChat replay and negative phrase regressions pass. Negated/quoted substring routing remains a separate known limitation; deployment pending.

Co-Authored-By: GPT-6 <noreply@openai.com>
Protect literal state and wake shortcuts from immediately negated commands and paired quoted mentions. Preserve quote context during ASR punctuation normalization. Exercise the actual ASR-to-MCP boundary with affirmative and escape regressions; indirect and hypothetical language remains outside this bounded guard.

Co-Authored-By: GPT-6 <noreply@openai.com>
Local safety-review candidate only; not deployed. Deny physical voice capture and cached voice descriptions unless the shared guardian policy explicitly enables adult mode. Re-read per access and fail closed on policy errors. Keep ambient/admin camera behavior unchanged.

Regression evidence: red-first tests, 359 root tests plus 25 subtests and 250 behaviour tests passed; Compose validation passed. Human safety/red-team review and separate deployment approval remain required.

Co-Authored-By: GPT-6 <noreply@openai.com>
Remove workstation-specific host/path defaults from public harness code and documentation. Initialization fails before network access without --host or DOTTY_TEST_HOST; existing session config remains authoritative.

Co-Authored-By: GPT-6 <noreply@openai.com>
…eadiness

Map an opt-in lossless sidecar from the same bounded capture process, request float Pulse input, and reject existing outputs. Add explicit warm-listening prerequisites for state-command trials. Verify with synthetic dual-output capture and isolated runner tests; no hardware or volume changes.

Co-Authored-By: GPT-6 <noreply@openai.com>
Bound native-rate float decoding by evidence windows and preserve prompt clipping warnings independently of the quiet response tail. Include optional lossless sidecar measurements with explicit non-sample-aligned timing caveats; do not infer interaction verdicts. Replayed J02 metrics without changing historical evidence; all380 root tests and25 subtests pass offline.

Co-Authored-By: GPT-6 <noreply@openai.com>
Gate warm-case capture on the perception server's own clock: compute chat-listening status age from sensor_age_s, last_event_t and last_chat_status_t, so unrelated sensor events cannot make a stale listening flag look fresh. Missing, non-finite or inconsistent timestamps block the case as warm_listening_precondition rather than passing silently. Capture is a prerequisite for a new trial only; recovery after a long capture is separate. 108 overnight harness tests pass.

Co-Authored-By: Qwen3.8-Max <noreply@qwen.ai>
A device or admin abort set conn.client_abort and nothing on the nointent
chat path cleared it, so every later reply on that connection was cut off
until the robot reconnected. Reproduced on the physical robot 2026-10-05
(abort, then 'What is your name?' -> ASR + LLM ran, no TTS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Near-silence after Dotty finished speaking was transcribed as 'Thank you.'
(no_speech_prob 0.81) and answered. Real utterances captured on the
physical robot 2026-10-05 all scored <= 0.32. Gate at Whisper's default
0.6, configurable via ASR.WhisperLocal.no_speech_threshold (1.0 disables).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Docker host runs agent/post-release-automation's pi_client.py and
pi_voice.py (direct remember/recall/think_hard routing). Bring this branch
level with the deployment so further fixes do not revert it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PiClient spawned pi with no --system-prompt, so pi's built-in coding-assistant
prompt was the only identity the model saw and it leaked into speech (#177).
Pass personas/pi_voice.md via --system-prompt (DOTTY_PI_SYSTEM_PROMPT_FILE
overrides; a missing file keeps the old behaviour).

Offline A/B in the dotty-pi container, 8 samples each of a self-review prompt:
before 1/8 'coding assistant' and 4/8 off-topic; after 0/8 and 0/8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uator slips as faults

Refs #182. Opt-in --mic-opener admin_say makes a warm case open the microphone
through /xiaozhi/admin/say instead of blocking, so a session can run
unattended. Playback is also proven by the robot's own recognition; a
reference-mic mishearing contradicted by service TTS text and any extra
speech in the capture are INCONCLUSIVE; inconclusive attempts no longer count
toward quarantine or the three-failure pause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… turner

Dashboard pieces still pointed at producers that left the bridge in the
#36 / #111 cutovers:

- Activity feed opened /api/perception/feed on the bridge (404). The
  bridge now relays dotty-behaviour's SSE stream at /ui/perception/feed.
- Emoji / Say / Story / Dance waited 8 s on a turn stream nothing
  publishes to, then reported "no reply". They now return as soon as the
  inject is accepted.
- Voice tools card listed 5 of the 7 dotty-pi-ext tools; a test now
  pins the list to dotty-pi-ext/src/tools/.
- Bridge detail said "Raspberry Pi"; robot "Last seen" ignored chat
  activity now that turns no longer land in the bridge's convo log.

Sound localizer (#27, still stuck-left):

- Remove the sound-balance sparkline from the State card along with its
  getter and dotty-behaviour's /api/perception/sound-balance endpoint.
- Gate SoundTurner behind SOUND_TURN_ENABLED (default off) — every
  sound_event reports direction "left", so it snapped the head left on
  any noise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first pi_voice persona fixed the coding-assistant leak but made the 4B
chattier: offline, 'repeat these five words' was refused or hedged 3/8 and
'just say the answer' gained follow-up questions. With an explicit brevity
rule: repeat 8/8 exact, arithmetic 4/4 bare answers, self-review 8/8 about
itself with no coding-assistant mention.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dashboard's Turns tab, error toast, Errors count and error report all
read conversation turns the bridge stopped seeing when voice moved to
dotty-pi (#111): /ui/events had subscribers and no producer, and the
convo log the Errors readers parsed was never written. The Content
filter card had the same gap for kid-mode hits on the voice path.

- PiVoiceLLM posts each completed turn to the bridge at
  POST /api/voice/turn and each kid-filter hit to
  POST /api/voice/filter-hit. Fire-and-forget on a daemon thread with a
  2 s timeout, so a missing dashboard never delays or breaks a turn.
  Target is DOTTY_DASHBOARD_URL, else the dotty-behaviour host on :8081.
- The bridge keeps the last 200 turns in memory only (nothing on disk),
  fans them out on /ui/events, and the Errors count / report / last-seen
  now read that ring instead of the dead convo log.
- Both endpoints require X-Admin-Token when DOTTY_ADMIN_TOKEN is set.

Closes #183

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pt is not extra speech

Refs #182. The unattended trial retried one case 11 times because every
attempt was INCONCLUSIVE (the reference mic always mishears one word of the
reply), which neither completes nor fails a case. After max_inconclusive (3)
such attempts the case moves to checkpoint['unresolved'] and the run moves on.
Also: the first recognition in a capture is the prompt even when misheard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BrettKinny and others added 30 commits October 21, 2026 11:32
Code review (2026-10-21, slice A): the direct think_hard route yields the
27B reasoner's answer verbatim. The reasoner's only instruction is 'answer
concisely', and since 122de9c the kid rules, English-only and length caps
live only in the 4B's system prompt, so in Kid Mode only the regex output
filter stood between the reasoner and the child. Before this PR the
answer went back through the 4B.

In Kid Mode the answer is now passed to the 4B as REASONING RESULT context
and retold under the rules (the kid filter still runs on the result);
outside Kid Mode it is spoken directly as before. dotty-pi-ext strips the
new block from logged turns. Also corrects the comment on the inner output
guard: it is kept for its buffering (a turn that fails part-way drops its
partial reply), not as the only filter pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code review (2026-10-21, slice A): after 91f516c aborts a runaway turn it
returns 5 s later even if pi never sends agent_end. think_hard ignores the
abort signal and can wait 90 s on a cold 27B, so the aborted run's late
text and agent_end landed in the shared queue and ended the next voice
turn early ('(no response)', or 'agent_end before prompt-accept' ->
'brain offline').

On that timeout PiClient now stops the pi process; the next call respawns
it with the same flags and system prompt. Each stdout reader is bound to
the queue it started with, so a late frame from the old process cannot
reach the new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code review (2026-10-21, slice A): startToChat calls _note_barge_in
whenever Dotty is speaking, and the admin inject-text route (used by
dotty-behaviour's face greeter and the dashboard) and the end-of-session
prompt both enter through it. Injected text could therefore count toward
#105's four-in-60-s storm and close a conversation, contradicting the
comment that admin paths do not count.

startToChat takes injected=True from those callers and skips the counter;
the ASR path is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f each message'

Code review (2026-10-21, slice A): since 122de9c the voice rules follow
the persona in pi's system prompt and the user's words arrive bare, so the
persona's closing line pointed the 4B at rules that are no longer there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on it

Code review (2026-10-21, slice B): _load_state accepted the persisted file
only if every device held one of the six durable states, so one probe that
posted state 'testing' discarded the robot's saved sleep/security state
too, and a non-UTF-8 file or a list-valued state raised past the
(OSError, JSONDecodeError) handler and stopped dotty-behaviour starting.
Drop invalid entries individually and start empty, with a warning, on any
unreadable file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e policy

Code review (2026-10-21, slice B): vision_explain's get_kid_mode() and the
ProactiveGreeter read app.state.kid_mode, which is set to False at startup
and never changed, so every VLM description (room views included) and
every proactive greeting used the adult prompt even in Kid Mode, the
default. Add config.kid_mode_on(), which reads the shared guardian file on
each call and fails closed (missing or unreadable means Kid Mode), and use
it for both. Brett chose to keep room views in Kid Mode with the
child-safe prompt rather than turn them off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dotty-behaviour hard-coded /mnt/user/appdata/dotty-bridge/state for the
read-only Kid Mode policy mount; with the bridge state elsewhere the file
is missing and the camera fails closed for ever. Use
${DOTTY_BRIDGE_STATE_DIR:-…} with the same default (DOTTY_STATE_DIR already
names this daemon's own state dir here).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lookup

Code review (2026-10-21, slice B): _perception_last_chat_ts read every
device in the perception state, so a probe or the 'unknown' id could mark
the robot 'active' while it was offline; it now goes through
candidate_devices like every other device read. The status strip also made
an uncached xiaozhi admin call on every render (every perception event, in
every tab); cache it for _PROBE_TTL like the TCP probes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olders

Slice D/C review (2026-10-21): dashboard_smoke.py defaulted to the
maintainer's dashboard IP, a test and a test skip message used the same
IP, and a new dashboard test used the robot's real MAC. The smoke script
now takes the URL as an argument or DOTTY_DASHBOARD_URL and refuses to
guess; tests use TEST-NET (192.0.2.x) and a made-up MAC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Slice D review (2026-10-21), fixed partly by a subagent and reviewed:
- docs/dotty-overnight.md carried the maintainer's SSH login (root@tower)
  and the robot's MAC; now <XIAOZHI_USER>@<XIAOZHI_HOST> and <DEVICE_MAC>.
- README: the default vision model is a cloud API (OpenRouter), not local;
  the output filter covers every LLM voice reply but not server-pushed
  speech (greeter, admin say); the rules live in pi's system prompt.
- kid-mode.md, troubleshooting.md, llm-backends.md, protocols.md, brain.md,
  modes.md: the per-turn 'sandwich' is now pi's system prompt (respawn on a
  Kid Mode change, tool-call cap); OpenAICompat still appends the rules.
  Danger tier described as an invitation-to-copy backstop; rules 1, 2, 7
  and 4-12 numbering match the code.
- SETUP, quickstart, troubleshooting, reproducible-builds, COMPATIBILITY,
  firmware-release.yml: earlier fw-v* binaries were LAN-locked and removed;
  build from source; the release workflow is manual-only (#190).
- uat-runbook UL3: the Kid Mode camera denial now exists.
- dotty-overnight-clips.md: --crop trims source pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view

Slice C review (2026-10-21), fixed by a subagent and reviewed; each fix
has a regression test that fails on the old code.
- dotty-av-test.sh run mode no longer unmutes or sets the speaker; it
  re-checks the calibrated level right before playback and exits 3.
- A FAIL verdict can never count as an acoustic pass (three late errors
  used to mark a case complete).
- S05: the held sleep state is judged from a snapshot taken before
  cleanup wakes the robot, so it can pass.
- A mic-transcript match no longer passes when the logged server TTS
  disagrees with the expected reply (INCONCLUSIVE).
- Forbidden content spoken is FAIL even when the prompt was misheard or
  someone else spoke.
- The reply-crop lag comes from a segment that holds prompt words, is
  capped at 1.5 s, and never crops before the prompt's last words.
- An operator stop, the session deadline or a harness timeout is
  INCONCLUSIVE, not a quarantining failure (crash recovery unchanged).
- wake_precondition is transient; the integration check retries a kid
  toggle restore and prints a loud banner if it never lands.
- Evidence drops recognised identities (perception snapshots and
  face_recognized events).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An abort sets conn.client_abort, and upstream's TTS thread drops every
queued message while it is set. Nothing cleared it until the next chat
turn or a reconnect, so server-pushed speech after any abort was lost
without an error. The face greeter, the proactive greeter and the A/V
harness's mic opener all speak through this route; the 2026-10-22 run
caught it when "Ready." after a song's cleanup abort never played.

The say route now clears the flag before queueing, as play-asset already
does. The aborted turn's leftover text is still dropped by the
sentence_id swap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
The server's speech recognition misheard "Zorbo Quill" on every T03
attempt in the 2026-10-22 run ("Orbo film", "both quills", "orb of
quill"), so recall_person never had a name to look up. T04 and T03 now
use "Sam Baker" and blueberry pancakes; the pending-review checks are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
In the 2026-10-22 run the 4B solved the wugs puzzle correctly ("Not
necessarily…") without calling think_hard, and V07 failed for it. The
answer is what a user hears. T05 already shows think_hard works when
asked for, and pushing the routing rules toward the tool is what caused
this week's 19-call think_hard loop. V07 now passes on the right answer
and still fails a wrong one; any think_hard call is recorded in
observed_tool_calls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
Asked "What do you know about my friend Sam Baker?" a second time in the
same conversation, the 4B answered "I don't have any information" from
its earlier lookup without calling recall_person (2026-10-23 A/V run,
T03). Memory can change between replies (a pending fact approved on the
dashboard, say), so the routing rules now ask for a fresh lookup on every
question about a person or something remembered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
… my friend X"

In the 2026-10-23 A/V run the 4B answered "What do you know about my
friend Sam Baker?" with "I don't have any information" and no
recall_person call, even in a freshly spawned pi. 25dcb7c blamed reuse of
an earlier lookup, but every voice turn already starts a new pi session,
so that rule is removed again.

Like the explicit remember / recall / think-hard phrasings (#165), a
question about a named friend, neighbour, teacher or relative now runs
recall_person itself. Nothing known gets a fixed "I don't know anything
about <name> yet" without a model turn. recall_person never reads pending
facts, so that is also what's said about a fact still waiting for review.
Facts that are known go to the model as context, under the
MEMORY SEARCH RESULTS marker the extension strips from the logged turn.
"What do you know about dinosaurs" still goes to the model.

The A/V harness's T03 accepts the direct-route log line as evidence, as
T06 does for take_photo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
…ecide (#192)

"Please put on the song called tetris" got "I can't play Tetris because
it's a puzzle game, not a song" with no play_song call (2026-10-23 A/V run,
T07; the same failure on 2026-10-20). "Play/put on the song (or tune)
called|named <x>" now runs play_song directly. The tool's catalogue match
decides, not the model:
- "playing tetris" becomes "Here's tetris!" and the song starts after the reply;
- no match becomes "I don't have a song called <x>.";
- anything else becomes "I couldn't play that song right now."

"Can you play a song?" with no name still goes to the model. The A/V
harness's T07 accepts the direct-route log line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
"What is 12 plus 10 room?" got "12 plus 10 is 22" in two of three tries
in the 2026-10-23 A/V run (K02; #178 saw "22 room"). Rule 10's "a question
gets its answer" won over rule 11's "ask again". Rule 10 now says a *clear*
question gets its answer, and rule 11 says a word that doesn't belong
means part of it was misheard: ask again instead of half-answering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
This reverts commit 53d8cd5. On the robot it made no difference: "What is
12 plus 10 room?" still got "12 plus 10 is 22" in 2 of 2 tries. V03 was
unaffected. The rules stay as they were, and K02's pass criterion goes to
the maintainer instead of more prompt pressure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
S06 (dance) is speech-optional, but the runner honoured that only when a
spoken reply couldn't be transcribed. In the 2026-10-23 run the firmware
danced (talk -> dance -> idle) without any spoken reply, and the attempt
failed as no_tts. no_tts now gets the same treatment: it passes only with
the expected state observed and audio above the case's level floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
Dotty greets the wake word straight away ("Hi ESP. How can I help you
today?", enable_greeting). W01 spoke "Hi E S P. What is your name?" in
one breath, so the question landed on top of the greeting and recognition
heard nothing. It has never passed.

A case can now set prompt_pause_after / prompt_pause_seconds. The renderer
voices both parts and joins them with silence, and the pause is recorded in
the renderer label, so changing it re-renders. W01 pauses 5 s after the wake
word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
Greetings ran 2.4-4.2 s, but one reached 6.3 s and overran the 5 s pause
(2026-10-23 A/V run).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
W01 played its wake word 4 s after Dotty's conversation timed out, while
the robot was still reconnecting its WebSocket, and the wake word was
missed (2026-10-23 A/V run). The attempt after it, on a robot already
cold, woke at once. When await_cold has had to wait, it now gives the
robot 15 s and checks it's still cold before returning. A robot that is
already cold isn't kept waiting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
The model writes the greeting fresh each time; one ran to 7.3 s, up against
the 8 s pause (2026-10-23 A/V run).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
…can wake it

docs/modes.md says the dashboard (admin set-state idle) wakes a sleeping
Dotty, but in the 2026-10-23 A/V run (S05) it couldn't:
- cdce1df made an admin sleep close the WebSocket;
- a sleeping robot never reconnects, because voice, wake word and
  perception are all gated off;
- so set-state idle had no device to reach;
- even without that close, xiaozhi's inactivity check (no-voice time + 60 s)
  drops a device that sends no audio.

Changes:
- admin sleep no longer ends the session (idle still does);
- on a state_changed=sleep event, keep_alive_while_asleep refreshes the
  connection's last_activity_time every 30 s until the state changes or
  the connection stops.

The firmware privacy gate keeps the mic and wake word off while asleep, so
holding the socket open hears nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
…keeps its voice

After a restart in the 2026-10-23 A/V run, the robot connected twice in
the same second:
- both connections built their TTS on worker threads;
- upstream's create_instance treats any sys.modules entry as loaded, so
  one connection got the half-imported piper_local ("partially
  initialized module ... has no attribute 'TTSProvider'");
- the connection that stayed had no TTS, so /admin/say returned 503 "device
  has no tts provider" and Dotty could not speak until it reconnected.

WebSocketServer now imports the configured TTS module once on the main
thread before accepting connections. This is best effort: any problem is
left for the per-connection path to report as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
S05 checks that a sleeping Dotty says nothing. With the reply hotword
"Dotty", Whisper heard a lone "Dotty" in the silent crop twice in the
2026-10-23 run. The server had logged no speech and no reply, but the
attempts came out inconclusive (speech_in_silence_window). Silence cases
now transcribe without the hotword.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW
Moves the CHANGELOG's Unreleased entries under [server-v0.2.0-rc.1] -
2026-10-24 and bumps the code-level versions (dotty-pi-ext 0.2.0-rc.1,
dotty-behaviour VERSION 0.2.0-rc.1). Docker image tags stay at 0.1.0:
they're shared with the deploy scripts and the host compose files.

The tag itself is the maintainer's to cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRqp6NDnGKg1DWFd9qrkjW

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Voice: one abort frame silences every later reply until reconnect

2 participants