All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog. From v0.1.0 forward this project follows Semantic Versioning. Server and firmware tag independently as server-vX.Y.Z and fw-vX.Y.Z; see COMPATIBILITY.md for the matrix.
- Kid Mode: blocked-words content filter on the live voice path (#157, closes the gap tracked in #138) — the pure matcher (three severity tiers + the kid-safe replacement) moved from
bridge/text.pyinto the sharedcustom-providers/textUtils.py, the single source of truth both containers import; the bridge keeps its metrics/safety-ring/logging wrapper on top with unchanged behaviour. Both live LLM providers (PiVoiceLLM,OpenAICompat) now wrap their TTS-bound streams infilter_tts_stream(). In Kid Mode it drains and checks the complete response before TTS, then emits the original clean chunks or atomically replaces a blocked turn; outside Kid Mode it remains a transparent streaming passthrough. Full-turn consumption also prevents PiVoiceLLM from abandoning the RPC iterator beforeagent_endand leaking stale frames into the next turn. Honest caveat (seedocs/faq.md): a word-level blocklist is a weak, bypassable backstop — prompt steering remains the primary defence. Bench: needs on-device red-team verification before release sign-off. - Admin-API auth:
X-Admin-Tokenacross the whole stack (#149, #150, #151, #152) — xiaozhi-server's/xiaozhi/admin/*routes now accept anX-Admin-Tokenshared secret (DOTTY_ADMIN_TOKEN, timing-safe compare, permissive when unset), and all three callers — the bridge dashboard, dotty-behaviour'sXiaozhiAdminClient, and the dotty-pi voice tools'adminFetch— send it. The secret is now plumbed end-to-end:make setupgenerates one into.env, the compose template / all-in-one pass it to xiaozhi-server, the three service compose files document where their copy lives, and.env.example+SETUP.md§10 describe the enable-everywhere-or-nowhere semantics. Previously the code shipped with no config path, so every deploy silently stayed permissive. - PersonResolver — one answer to "who is this?" (
dotty-behaviour/household/resolver.py) — identity resolution was smeared across consumers, and the 2026-06-06 audit found four separate identity bugs because of it. All resolution now funnels through one module withPerson.idas the canonical key space. Fixed by the consolidation: room_view roster recognition silently failing wheneverid != display_name(the VLM echoes display names; validation compared ids — confirmed 3/3, both the core and greeter paths), multi-word display names never matching (the NAME parser was single-token, so "Mary Anne" was a 100% silent miss — confirmed 3/3), the greeter's calendar lookup dropping a person's own events on a case mismatch ([Hudson]≠hudson— confirmed 2/3), and bracketlesscalendar_prefix:YAML never matching.summarize_for_promptnow matches person tags case-insensitively and accepts the resolver's tag set; the room_view test fakes were also fixed to carry real ids (the old fakes re-derived ids from display names, which is exactly what masked the bug). - DeviceCommand — one seam for server→firmware MCP tool calls (
custom-providers/xiaozhi-patches/device_command.py, mounted atcore/utils/device_command.py) — twelve call sites across the/xiaozhi/admin/*handlers andreceiveAudioHandle.pyeach hand-rolled the same JSON-RPC envelope, so every shared defect was twelve defects (2026-06-06 audit): request ids wereint(time.time()*1000) % 0x7FFFFFFF(same-millisecond calls collided, with zero reply correlation to notice), and every site firedconn.websocket.send()uncoordinated against the other senders on the same connection. The seam owns monotonic per-connection request ids, the MCP envelope, and per-connection serialized sends (an asyncio.Lock — play-asset's opus frames now route through it too, so a concurrent head-turn can't interleave mid-frame). Admin handlers also share one_dotty_resolve_conn()instead of eight copies of the device-lookup block (which fixes the audit'sinject-textmissing-or {}headers nit as a side effect). Reply correlation is deliberately still absent —call_toolreturns the request id so it can be added behind this interface without touching the callers again. Bench: needs a live-device smoke (LED pips, head-turn, take-photo, play-asset) before release sign-off. - Opt-in
sensevoice_onnxASR provider (#135, builds on #124) — a newSenseVoiceOnnxprovider (custom-providers/asr/sensevoice_onnx.py, typesensevoice_onnx) running an int8 sherpa-onnx / ONNX Runtime export of SenseVoiceSmall with no PyTorch dependency (~230 MB vs ~900 MBmodel.pt). It's a drop-in alternative to the patched FunASR provider for Pi-class / low-RAM hosts; FunASR stays the no-GPU default (flipping the default is a separate, benchmark-gated follow-up). Select viaselected_module.ASR: SenseVoiceOnnx; keepslanguage: en.make fetch-modelsandmake doctornow also download and size-checkmodels/SenseVoiceSmall-onnx/(model.int8.onnx+tokens.txt). - Bridge systemd unit loads API keys from
${BRIDGE_DIR}/.env(#15) —zeroclaw-bridge.service.templateandscripts/install-bridge.shnow emitEnvironmentFile=-${BRIDGE_DIR}/.env.install-bridge.shcreates a mode-0600 stub.envcontainingOPENROUTER_API_KEY=(and commentedVISION_API_KEY/VLM_API_KEYplaceholders) when one isn't already present, so the missing-vision-key failure surfaces as the bridge's existing ERROR ("camera offline") instead of a silent confabulation. Existing.envfiles are preserved.
- Whole-repo vision-alignment pass (2026-05-29) — a multi-agent audit found ~50 verified code/doc-drift findings from the post-#36 and #115 cutovers; this pass resolves them against six locked maintainer decisions (see
docs/vision-alignment-review-2026-05-29.md). Highlights: dashboard port references corrected to :8081 repo-wide (:8080is the llama-swap endpoint); perception re-attributed from the retiredbridge.py_perception_*methods to the 11 dotty-behaviour consumer classes; the brain's voice-tool count corrected to 7 (a nonexistentset_ledtool was removed from the docs, the realrememberrestored); kid-safety wording fixed to state the live PiVoiceLLM sandwich ships while the blocked-words content filter remains the open gap;make doctor/status/audit/setupanddotty_doctor.pyrepointed off the retired ZeroClaw RPi at:8080to the dashboard (:8081) and dotty-behaviour (:8090). - kid/smart-mode state shared across containers —
receiveAudioHandle.py(xiaozhi container) and the bridge dashboard now resolve the toggle state files to the same/var/lib/dotty-bridge/statemount, fixing a desync where the firmware LED pips drifted from the dashboard on every reconnect. - Documentation reconciled to the post-#36 architecture —
README.md,CLAUDE.md, and thedocs/tree previously described the retired ZeroClaw bridge and its Raspberry-Pi brain host. They now describe the live stack: thedotty-pipi-agent container (the voice brain, reached via thePiVoiceLLMprovider),dotty-behaviour(perception bus + ambient consumers + greeter, port 8090), andbridge.pyas the admin dashboard service (port 8081)..config.yaml.templateanddocker-compose.yml.templateupdated to match —vision_explain/VISION_BRIDGE_URLnow point at dotty-behaviour, and thezeroclawprovider mount +ZeroClawLLMconfig block are gone. The #36 cutover was executed 2026-05-19; this is the follow-up doc sweep its runbook deferred.
- Tier1Slim voice provider (
custom-providers/tier1_slim/,docs/tier1slim.md, its tests, and the/xiaozhi/admin/set-tier1slim-modelroute) — its tool escalation depended on the retired ZeroClaw bridge and was non-functional post-#36.smart_modeis now a toggle-only control on the live PiVoiceLLM path; backend model-swap is v2 scope.OpenAICompatremains the alternate provider. - Dashboard self-update / restart / reboot-all actions — they invoked a
systemctl restart zeroclaw-bridgeunit absent from the container and git-pulled into the dead/root/zeroclaw-bridgeinstall dir, so they no-op'd while reporting success. Deploys go throughscripts/deploy-bridge-unraid.sh; the header version chip is now a static label. custom-providers/zeroclaw/— theZeroClawLLMvoice provider, dead since the #36 cutover.docs/multi-daemon-split.md,docs/advanced/multi-host.md— both documented ZeroClaw-host topologies that no longer exist.
- No-GPU ASR path no longer crash-loops on first run (#124, #136) —
make fetch-modelswas requesting two SenseVoiceSmall filenames that don't exist on Hugging Face (tokens.jsonandchn_jpn_yue_eng_ko_spectral.fbank.conf.yaml); the real SentencePiece tokenizer ischn_jpn_yue_eng_ko_spectok.bpe.model. Both 404s were silently saved as 15-byte "Entry not found" stubs (curl had no--fail), so funasr loaded withbpemodel=Noneandxiaozhi-esp32-servercrash-looped on every GPU-less host. The file list is corrected; allfetch-modelsdownloads now fail loudly (curl --fail --retry+ a size floor + delete-on-failure) instead of saving junk; andmake doctornow size-checks the required SenseVoice assets so a corrupt download FAILs instead of passing. Huge thanks to @miltieIV2 — a meticulous bug report and a self-driven root-cause that pinned it on theHAS_CUDA=0FunASR switch. A lighter int8 sherpa-onnx SenseVoice runtime (no PyTorch) for Pi-class hosts has landed as the opt-inSenseVoiceOnnxprovider (#135; see the Added section above).
First git-tagged public release. Covers all server + firmware work shipped to main between project inception and 2026-05-17. The earlier [0.1.0] - 2026-04-25 entry below describes a pre-tag internal milestone — retained for historical reference, but server-v0.1.0 is the canonical first release.
- Two-tier voice path:
Tier1SlimLLM provider (b73f583,custom-providers/tier1_slim/tier1_slim.py) — slim inner-loop LLM in xiaozhi-server that runs a small/fast model (defaultqwen3.5:4bagainst llama-swap) for chitchat and escalates tool calls to the bridge viaPOST /api/voice/escalate. Tools:memory_lookup,think_hard,take_photo,play_song. Cuts plain-chat latency well below 1 s; reserves the heavy ZeroClaw / cloud path for tools that genuinely need it.set_runtime()allows hot-swapping model/url/api_key in flight (no daemon restart) — used by smart-mode flips. - xiaozhi-server admin endpoints (
custom-providers/xiaozhi-patches/http_server.py) —/xiaozhi/admin/play-asset,/xiaozhi/admin/songs,/xiaozhi/admin/set-tier1slim-model(hot-swap the running Tier1Slim provider; bridge calls this on smart-mode flip whenDOTTY_VOICE_PROVIDER=tier1slim).shared_llmsingleton inportal_bridge.pyexposes the live provider to the admin routes. - Help-intent handler (
1ccfdd6, xiaozhi) — voice "what can you do?" yields a curated capability summary instead of the model freelancing. - Persona library collapsed to default + smart (
3a055a6) — three earlier persona files reduced to two; dashboard simplified accordingly. - TTL-bound face identification (
5a3cab7,bridge.py) — bridge owns identified-face TTL with refresh loop; firmware face-pip flickers if TTL expires without refresh, ensuring stale identification doesn't pin the green pip indefinitely. - Vision capture modal (
6c8fb45, dashboard) — full-size vision capture in dashboard with download. - Sleep banner moved to Perception card (
74f8dc9, dashboard). - Dashboard state-card polling + kid_mode hot-load cleanup (
089c575). - Dashboard auto-refresh stabilised (
ae54e93).
DOTTY_VOICE_PROVIDERhot-swap landed (e2930ce,bridge.py) — smart-mode flips now pick their dispatch path based on the env var.=tier1slim→ in-process hot-swap via/xiaozhi/admin/set-tier1slim-model(no docker restart, no daemon restart, instant).=zeroclaw(default) → legacy~/.zeroclaw/config.tomlrewrite +systemctl restart zeroclaw-bridge. Same commit retargetedthink_hardtoqwen3.6:27b-thinkon llama-swap./xiaozhi/admin/set-tier1slim-modelendpoint (b83898e,custom-providers/xiaozhi-patches/http_server.py) — the receiving side of the hot-swap. Mutates the live Tier1Slim provider'smodel/url/api_keyviaset_runtime(). Refuses to blank a non-emptyapi_keyso a half-configured OFF→ON flip fails fast instead of 401-looping.- VLM fallback hardened (
aa2d8ba,bridge.py) — missing VLM API key now surfaces a clear no-vision message instead of letting the model confabulate a description with no image input. - llama-swap concurrent-models recipe shipped (
968949a,docs/cookbook/llama-swap-concurrent-models.md) — documentedvoicematrix set (qwen3.5:4b+qwen3.6:27b-thinkco-resident) andcodingmatrix set (qwen3.6:27bsolo) for thepiCLI. Avoids evicting the voice pair on coding sessions; cold-reload cost paid on next voice turn after apirun. - Voice local backend migrated Ollama → llama.cpp / llama-swap (
34552e3,zeroclaw-bridge.service) —VOICE_LOCAL_PROFILE_KEYbumped from:11434→:8080. 2.15× generation speedup (8 → 18 tok/s on dual RTX 3060), eliminates 2.7 GB of CPU offload, fits the model fully on GPU. Cold load ~20 s (was 70 s). - Bridge
VOICE_THINKER_TIMEOUT=90added to systemd unit template (452bbd7) — keeps longthink_hardescalations from being killed by the default request timeout. - Top-level reboot button removed from dashboard header (
3198b8e) — too easy to misclick; functionality remains accessible via Admin card.
- Phase 4 StateManager shipped (firmware
d78118b, bridge+xiaozhi10cbc63, 2026-04-27) —firmware/main/stackchan/modes/state_manager.{h,cpp}: six-state mutex (idle/talk/story_time/security/sleep/dance), state-arc paint on left ring 0-5, kid/smart toggle pips on right 8/9, face-state pip on right 6, listening pip on right 11, locked-off pixels at 7/10, 5 Hz re-assert tick, security 1 Hz flash, sleep torque release, MCPself.robot.{set_state,set_toggle,set_face_identified}handlers,state_changedperception event emit. End-to-end round-trip verified autonomously (POST/admin/state→ firmware →state_changedevent back; 13 → 15 MCP tools post-flash). Visual / interactive bench checks pending in #38. - Phase 5 sleep behaviour shipped — head face-down + centred, servo torque off, sleeping emoji, ambient awareness paused, wake on face/voice/head-pet. Bench checks: #39.
- Phase 6 security behaviour shipped — wide deliberate yaw scan (SURVEILLANCE idle profile), periodic photo + audio capture via bridge ambient task, greeter gate. Bench checks: #40.
- Privacy sleep extended to camera + mic (
ac51662,1754499) — enteringsleepstate now disables camera and routes mic-off through the xiaozhi privacy gate, not just the LED indicator. - Listening LED edge cleared on enter-sleep (
deca11e). - Sleep torque release with timeout fallback (
cd23282) — preferred path is settle-based release inStateManager::_update; 3 s timeout fallback when settle never fires. (Known issue: still being torqued in some cases.) - Face-identified flicker grace + perception event emit (
613a0ca) — addskFaceIdentifiedFlickerGraceMs = 1500to ride out brief detection hiccups; emits perception event so the bridge mirror updates. - AXP2101 PEK IRQ register addresses corrected (
5ea12e0) — long-press / power-button events now register at0x41/0x49, not0x42/0x4A. Previous addresses worked in many cases but missed the canonical IRQ status bits. - LEDs cleared before AXP self-off on long-press (
0736a1e) — clean visual shutdown. face_trackingWakeWordInvoke gated onGetDeviceState()(1775759) — kills the double-wake on flickering walk-in (face_tracking was firing WakeWordInvoke even when device was already inLISTENING).- kid_mode pip retuned salmon (220, 80, 80) (
dcad76f) — earlier hue (168, 80, 100) had B > G after RGB565 quantization, reading as cool purple/magenta. New hue keeps G == B; renders warm. - V4L2 ioctl EINVAL fixed (
37e92d6) — restored Linux_IORencoding after lwip clobbered it. Camera streams cleanly again. - HEADMOVE writer instrumentation (
1e30a05) — every head-write site (idle_motion, mcp_set_head_angles, keyframe_servo, head_pet, state_manager) now tags its writes for trace logging. Diagnostic-only.
- Firmware Phase 4–6 work landed on
BrettKinny/StackChan @ dotty(commitd78118bfor Phase 4 StateManager + later commits for sleep and security state behaviour), but thefirmware/firmware/submodule pin in this repo lags. Users flashing from the submodule will not get StateManager / set_state / set_toggle MCP handlers, the six-state LED contract, or the bench-pending behaviour for sleep / security states. Bump the submodule pin (or build from the active fork) to flash a Phase 4+ build. Visual / interactive bench checks tracked in issues #38 (Phase 4), #39 (sleep), #40 (security).
- dlib biometric face recognition —
bridge/face_db.py,bridge/face_recognizer.py, theface-recognitionrequirement, the/api/face/{enroll,recognize,forget,list,last-action}endpoints, the per-channel_voice_identity_pending/_identity_statemachinery, and the voice-driven enrollment / list / forget intents inreceiveAudioHandle.py. The description-based identity path (Layer 4 v1.5 — VLM returns a description plus a roster name match againsthousehold.yaml'sappearance:field) is now the sole identity feed. The biometric path was opt-in v2 only, never reached production (dlib won't build on Python 3.13 / DietPi), and conflicted with the project's no-storage identity posture. Firmware-sideFaceRecognizer+ParentalGate+ the inert call atface_detector.cpp:273will be removed in a follow-up firmware-only PR. - Blind mode v1 — time-based civil-dusk-to-dawn gating (
_is_blind,_civil_twilight_bounds,_blind_mode_gauge_refresher,dotty_blind_mode_activePrometheus gauge,DOTTY_BLIND_*env vars) removed in favour of a simple time-window guard on_perception_face_greeter(FACE_GREET_HOUR_START/FACE_GREET_HOUR_END, default 06–21). The walk-in soak revealed that the "too dark to see" reply was wrong indoors at night with lights on (modern VLMs handle indoor low light fine), and blocked legitimate vision use after dusk. Killing 3 AM "Hi!" greets is the only gate worth keeping; replaced with a 5-line hour check. - Phase 2 audio scene classifier (YAMNet) —
bridge/audio_scene.py,bridge/yamnet_classmap.py,tests/test_audio_scene.py,scripts/fetch-yamnet.sh,docs/audio-scene-classifier.md, the_audio_scene_*globals + thread-bridge helper inbridge.py, the/api/audio-scene/feedHTTP endpoint, lifespan startup/shutdown hooks, and the# tflite-runtime>=2.13optional dep comment. ~1058 LOC + 10 tests + 200-line docs page. Default-OFF scaffold (AUDIO_SCENE_ENABLED=false) shipped 2026-04-26 then sat dormant —tflite-runtimewas never installed on the ZeroClaw host, no xiaozhi-side forwarder ever materialised, and no production traffic touched the endpoint. Same speculative-scaffold pattern as the rich_mcp / engagement_decider rips. Hybrid smart-mode LED firmware-side (set_led_multiMCP tool,NeonLight::setColorAt) and bridge-side consumer (_send_led_multi,conn.smart_mode_active) survive — independently useful for smart-mode and unrelated to the classifier. The dependent "Dance when music is detected" task entry was removed at the same time. If audio-scene classification ever becomes a real product need, start from current state, not this scaffold.
- Length-aware brevity — voice replies default to 1-2 short sentences (was 1-3), but the model is now invited to take a fuller swing on open-ended asks ("tell me a story", "explain why X", "list some Y") up to 6 sentences. Enforced via
_BASE_SUFFIXrule 3 incustom-providers/textUtils.py, theVOICE_TURN_SUFFIX_SHORTreminders inbridge.py, and aMAX_SENTENCESdefault bump from 3 to 6 (still env-overridable).personas/{default,assistant,playful}.md+.config.yamltemplate +docs/kid-mode.md+docs/cookbook/disable-kid-mode.mdall updated to the new wording. Cheapest possible "model-from-context" change — no classifier, no trigger phrases, no server-side routing. Smart-mode bypass unchanged (Sonnet still answers at full length when invoked).
- Calendar polish (
bridge.py) —EventTypedDict +by_personcache, person-tag regex,summarize_for_prompt()single privacy chokepoint stripping ISO timestamps + emails before any prompt injection. NewGET /api/calendar/todayendpoint. Background poll loop with exponential backoff. Nightly-flush evicts stale events on date roll-over. - Voice catalog + installer (
docs/voice-catalog.md,scripts/voice-install.sh) — 12 Piper + 6 EdgeTTS voices curated.make voice-install VOICE=<key>andmake voice-list. - Observability (
bridge/metrics.py,monitoring/grafana-dashboard.json,docs/observability.md) — Prometheus/metricswith 9 metrics (first-audio latency histogram, request duration/errors per endpoint, ACP session gauge, smart-mode/kid-mode counters, perception event counter, calendar fetch failures). Two-layer defensive guard so metrics regression cannot break request path. - Layer 6 ProactiveGreeter (
bridge/proactive_greeter.py,bridge/server_push.py,docs/proactive-greetings.md) — face_recognized → cooldown + time-of-day windowing + kid-safe sandwich + calendar-aware greeting via inject-tts. Template fallback. 14 unit tests. - Hybrid smart-mode LED bridge half (
receiveAudioHandle.py) —_send_led_multihelper +conn.smart_mode_activeflag. Holds index 0 purple while the rest of the ring shows listen/think/talk. Re-asserts on every color change. try/except guarded for old-firmware compatibility. - Face greeter env-tunable —
FACE_GREET_TEXT(set "" to disable verbal greet) +FACE_GREET_MIN_INTERVAL_SEC(default 30s). - Purr-on-head-pet (server) (
bridge.py,bridge/assets/) —_perception_purr_playerconsumeshead_pet_started, pushes purr audio via inject-text. Per-device cooldown. Bypasses kid-mode sandwich (fixed asset). Asset path is a drop-in (not committed; seebridge/assets/README.md). - Server-side Layer 4 face recognition (
bridge/face_db.py,bridge/face_recognizer.py) — Option B fallback to the on-device path. - Household roster (
bridge/household.py,household.example.yaml) — family roster with per-person config. - Speaker voiceprint (
bridge/speaker.py) — voiceprint speaker identification module. - Wake-word options doc (
docs/wake-word.md) — current architecture, 21 prebuilt English wake words, three paths to "Hey Dotty" (Path A interim shipped, Path B microWakeWord roadmap, Path C wakenet9 custom). Sample collection guide. - SBOM scaffold (
scripts/generate-sbom.sh,docs/sbom.md) — CycloneDX-ish component+license inventory.make sbom. - Signed releases scaffold (
docs/signed-releases.md,KEYS.txt) — GPG signing walkthrough + CI integration snippet (commented-out signing step ready to enable).
- Layer 1 privacy LEDs scaffold —
PrivacyLedssingleton drives right-ring index 6 (mic) + index 7 (camera). RAIIMicPeripheralGuard+CameraPeripheralGuardtie LED state to peripheral enable codepath. Newself.robot.get_privacy_stateMCP tool.set_led_multirejects indices 6/7. - Layer 4 face recognition scaffold —
FaceRecognizer(NVS-backed, max 10 enrolled, embedding stub until ESP-DLface_recognition.sois wired).ParentalGate(PIN + long-press, single-shot 30s token). 4 MCP tools:face_unlock,face_enroll,face_forget,face_list. Newface_recognizedperception event. - Hybrid smart-mode LED firmware half —
NeonLight::setColorAtpublic +self.robot.set_led_multiMCP tool. - Head-pet hold-to-listen wake — touch ≥2s →
WakeWordInvoke("head_pet_hold")opens listen window. Works in the dark. Also emitshead_pet_started/head_pet_endedperception events for the purr consumer. - Wake-word default switched —
sdkconfig.defaults: Chinese "Hi, Stack Chan" → English "Hi, ESP". Interim while custom "Hey Dotty" microWakeWord is being trained.microwakeword_setup.mddocuments long-term plan.
- Face tracking smoother + faster — EMA alpha 0.3→0.5,
lookAtNormalizedspeed 350→500, 6% bbox-center deadband. MSR threshold 0.25→0.40 cuts stage-2 work for marginal candidates. All knobsconstexprfor one-line revert.
- Camera arbiter TOCTOU race — fold flag check inside mutex region, eliminating 2s stall window.
- Stale
idle_motion_modifier_id_inFaceTrackingModifier— lookup by stable name at call time instead of caching ID at construction. AddedModifier::name()virtual +StackChan::getModifierByName()API.
- Rich MCP tool surface (
bridge/rich_mcp.py,bridge/rich_mcp_dispatch.py,docs/rich-mcp.md, 13 tests). Never enabled in production (DOTTY_RICH_MCP=falsedefault). Cut as dormant scaffolding — voice-only is the intended product surface; don't re-add. - Phase 4 EngagementDecider (
bridge/engagement_decider.py,bridge/intent_templates.py,docs/engagement-decider.md, 32 tests). Never enabled in production (ENGAGEMENT_ENABLED=falsedefault). Cut for the same reason. Proactive utterances remain served bybridge/proactive_greeter.py. docs/mcp-tools-capture.jsontrimmed 17 → 13 tools — the 4robot.face_*entries were rich_mcp fabrications (firmware actually exposescamera.face_*and has noface_unlocktool at all).set_led_multiandget_privacy_stateretained as real firmware tools.
- Camera
VIDIOC_STREAMOFFperipheral-off when face-detect is paused (closes the Layer 1 privacy LED hole noted ineb595f2). Status 2026-05-15: superseded byac51662privacy-sleep camera disable — the broader privacy posture now covers this hole at sleep entry, though pause-aware streamoff is still a finer-grained want. - Reproducible firmware builds — IDF Dockerfile SHA256 pin +
dependencies.lock+make verify-firmwaretarget.
Originally written as a release entry, but never actually tagged. Retained here as a snapshot of what shipped by 2026-04-25; the full v0.1 surface is in the [server-v0.1.0] entry above. Works end-to-end on the maintainer's hardware (M5Stack StackChan + Docker host + ZeroClaw host + ZeroClaw + OpenRouter Mistral Small 3.2). External users welcome; see ROADMAP.md for known issues.
- Smart Mode marker check.
zeroclaw.py_payloadwas matching[SMART_MODE]\nagainst the composed[Context] … [User] …payload (marker landed at offset ~2700, sostartswithwas always False). Every voice "smart mode" turn since434988dsilently fell back to the default voice model. Fix detects markers on the raw user message before_compose()wraps it.
- Default LLM switched from
qwen/qwen3-30b-a3b-instruct-2507tomistralai/mistral-small-3.2-24b-instruct(2.6× speedup, p50 1.9 s vs 5 s, no quality regression on smoke battery). - Rebranded to Dotty. Project identity renamed from
stackchan-infrato Dotty (dotty-stackchan). Default robot name is "Dotty" (customizable viamake setup). Channel identifierstackchan→dotty(both accepted during transition). Python constantsSTACKCHAN_TURN_*→VOICE_TURN_*. All docs, config, and build files updated. - 3-sentence response limit enforced in both
/api/messageand/api/message/streamendpoints.MAX_SENTENCESenv var (default 3). - Streaming
finalline now always includes emoji prefix correction.
- ASR noise filter —
_is_noise()rejects punctuation-only or very short ASR results before they trigger a thinking animation or LLM call. Configurable viaMIN_UTTERANCE_CHARS. - ASR name correction —
_apply_asr_corrections()fixes common SenseVoice misrecognitions of the robot name. - Content-filter test probes — 10 new adversarial prompts targeting the
_BLOCKED_WORDS_REregex filter. - Custom LLM provider (ZeroClawLLM) —
zeroclaw.pyproxies xiaozhi-esp32-server LLM calls to the ZeroClaw agent on the ZeroClaw host via the FastAPI bridge. - FastAPI bridge (
bridge.py) — HTTP-to-ACP translator on the ZeroClaw host; speaks JSON-RPC 2.0 over stdio to a long-runningzeroclaw acpchild process. - ACP session caching — reuses a single ZeroClaw session across turns instead of creating/destroying one per request; rotates on idle timeout, turn count, or wall-clock age. Shaves ~1-2 s off first-audio latency.
- NDJSON streaming endpoint —
/api/message/streamstreams tokens as newline-delimited JSON so TTS can start on the first sentence while the LLM is still generating. - Streaming EdgeTTS provider (
edge_stream.py) — custom xiaozhi-server TTS provider using Microsoft Edge Neural voices with streaming audio delivery. - Local Piper TTS provider (
piper_local.py) — offline-first TTS alternative usingpiper-tts(en_GB-cori-medium); drop-in replacement for EdgeTTS with no cloud dependency. - FunASR English language pin (
fun_local.py) — patched ASR provider adds alanguageconfig key so SenseVoiceSmall can be pinned to English, preventing mis-detection of short utterances as Korean/Japanese. - Emoji emotion protocol — three-layer enforcement (ZeroClaw agent prompt, xiaozhi system prompt,
_ensure_emoji_prefixfallback inbridge.py) ensures every LLM response starts with an emoji that the firmware parses into a face animation. - Thinking emotion frame — emits
{"type":"llm","emotion":"thinking"}to the device between ASR completion and the LLM call so the avatar shows a thinking face during the wait. - Child-safety enforcement sandwich — five numbered rules in
VOICE_TURN_SUFFIX(audience framing for ages 4-8, forbidden-topic list, roleplay-lock, profanity-lock, ambiguity tie-breaker) injected at max-attention position for Qwen3 compliance. Tier 1 of a pre-designed four-tier lockdown plan. - Self-harm routing rule — dedicated rule routes self-harm disclosures to a trusted adult instead of a generic cheerful redirect.
- Technical documentation suite (
docs/) — eight linked markdown files covering architecture, hardware, voice pipeline, brain, protocols, latent capabilities, and upstream references. - Docker packaging for zeroclaw-bridge — multi-stage Dockerfile (Rust builder to python:3.12-slim runtime), deploy-side compose file, and GitHub Actions workflow publishing multi-arch images (amd64 + arm64) to
ghcr.io/brettkinny/zeroclaw-bridge. - Dual deployment paths — both bare-metal systemd and Docker deployment for the bridge, sharing the same
~/.zeroclaw/state directory. - Placeholder-based configuration — all real IPs, usernames, and paths replaced with named placeholders (
<XIAOZHI_HOST>,<ZEROCLAW_HOST>,<ROBOT_NAME>, etc.) for safe public sharing. - systemd unit (
zeroclaw-bridge.service) — bare-metal bridge deployment withRestart=on-failure. - docker-compose.yml — container definition for xiaozhi-esp32-server with volume mounts for all custom providers.
- Depersonalized repo — renamed from "Dotty" to a generic StackChan stack; persona name is now user-configurable via
<ROBOT_NAME>placeholder. - Default LLM endpoint switched to streaming —
.config.yamlnow pointsZeroClawLLM.urlat/api/message/streamby default; the buffered/api/messageendpoint remains available for backward compatibility and smoke tests. - TTS mounts switched to flat-file format — directory-form mounts silently fell through to "unsupported TTS type" errors; now matches the working
fun_local.pyASR pattern.
- Abort race condition — kill and respawn ACP child on barge-in to prevent stale chunk contamination.
- FunASR language mis-detection — upstream hardcodes
language="auto", causing SenseVoiceSmall to classify short/unclear English audio as Korean or Japanese. Config-driven language override resolves this. - Child-safety self-harm response — LLM was redirecting to blanket-fort building instead of naming a trusted adult; dedicated rule fixed the last failing red-team case (10/10 pass rate).
- TTS provider loading failure — directory-form Docker mounts caused silent fallthrough; flat-file mounts fixed "unsupported TTS type" errors at connect time.