Symptom. The pi voice tools do not fire on the live voice path. The outer model answers
directly (and hallucinates memory acks) instead of calling tools.
Verification (2026-07-11, code + live-container probes). Root cause isolated with a live
reproduction; two corrections to the original finding.
- Extension loads and all 7 tools ARE registered — ruled out a wiring failure. A live
probe with the exact production flags (pi --mode rpc --provider ollama --model qwen3.5:4b --no-session --no-context-files --offline --no-skills ...) lists all 7 voice
tools plus 4 pi coding built-ins (read, bash, edit, write) that are never disabled.
With an explicit "use the remember tool now" instruction, the tool fires and executes
end-to-end — the dispatch path is fully functional.
- Live repro of the failure: replaying the exact UAT purple-turn (JSON-wrapped user
text + kid-mode HARD-CONSTRAINTS suffix) produced 43 frames, zero toolCall frames,
and the same hallucinated "😊 I've saved your favourite colour as purple!" reply.
- Correction 1 — the "no brain.db write" evidence was a WAL artifact.
grep purple brain.db = 0 but grep purple brain.db-wal = 6: SQLite WAL held the un-checkpointed
rows. The extension's per-turn agent_end conversation logger wrote ~46
voice_conversation_* rows during the session (proving the extension ran on every turn).
What's genuinely absent is any voice_core_* row — the remember tool was never
called; the verbal ack was hallucinated. UT1/UT2/UT6 conclusions stand.
- Correction 2 — UT7/UT8 "working tools" never went through pi. Vision and sing/song
requests are intent-detection shortcuts inside xiaozhi-server
(receiveAudioHandle.py:1093-1098 and :589-726) that bypass pi's tool loop entirely
(the photo turn reached pi as an injected [You just used your camera...] message).
Zero pi tool calls occurred in the entire UAT session.
Why the 4b doesn't call tools (contributing causes, in rough order):
- The per-turn HARD-CONSTRAINTS suffix ("Begin your reply now", "EXACTLY ONE emoji ... as
the first character", 1–2 short sentences) actively steers toward an immediate text
reply; a tool call is in tension with "first character is an emoji".
- Tool descriptions hedge ("Use sparingly", "Use only when the quick chat path can't
handle it") and --thinking off removes deliberation.
- No persona/system prompt reaches pi (
--no-context-files; only the last user message is
forwarded) — pi runs on its default coding-agent system prompt.
models.json gives qwen3.5:4b a 4096-token context; with 11 tool defs + system
prompt + ~450-token suffix, compaction fired twice on a single fresh turn.
- User text arrives JSON-wrapped (
{"content": ...}) — pi_voice.py:88-95 forwards the
raw JSON instead of unwrapping like receiveAudioHandle.py:1043 does.
Also fix (logging-visibility gap, confirmed mechanism). docker logs dotty-pi is empty
by construction: PID 1 is sleep infinity and each RPC turn is a docker exec child whose
stdio goes to PiClient. --no-session disables pi's own transcripts, and pi_client.py:265
logs unknown frames only at DEBUG. Suggested: log toolCall frames at INFO in
PiClient, and/or drop --no-session/tee transcripts.
Side finds. XIAOZHI_HOST is unset in the dotty-pi container, so the pi play_song
tool would fail if it ever were called (play_song.ts:104); a PiClientError: new_session timed out at 19:25:05 (pre-UAT) shows the RPC channel has reliability wobbles.
Expected. Keepable facts write to brain.db (category=core); hard questions swap to
qwen3.6:27b-think with a visible pause.
Original log excerpt.
19:57:39 大模型收到用户消息: "...my favourite colour is purple, please remember that."
19:57:40 发送第一段语音: I'll remember that purple is your favorite color # 0.6s, no tool
20:00:25 大模型收到用户消息: "Dotty, what is 47 times 89?"
20:00:25 发送第一段语音: 47 times 89 equals 4183 # 0.7s, no 27b-think swap
From the filmed UAT session 2026-07-11 (uat-sessions/2026-07-11/: results.csv, container logs, two video recordings). Investigated and drafted with AI assistance (Claude Fable 5 via Claude Code); verification included read-only live-container probes. Human-reviewed before filing.
Symptom. The pi voice tools do not fire on the live voice path. The outer model answers
directly (and hallucinates memory acks) instead of calling tools.
Verification (2026-07-11, code + live-container probes). Root cause isolated with a live
reproduction; two corrections to the original finding.
probe with the exact production flags (
pi --mode rpc --provider ollama --model qwen3.5:4b --no-session --no-context-files --offline --no-skills ...) lists all 7 voicetools plus 4 pi coding built-ins (read, bash, edit, write) that are never disabled.
With an explicit "use the remember tool now" instruction, the tool fires and executes
end-to-end — the dispatch path is fully functional.
text + kid-mode HARD-CONSTRAINTS suffix) produced 43 frames, zero toolCall frames,
and the same hallucinated "😊 I've saved your favourite colour as purple!" reply.
grep purple brain.db= 0 butgrep purple brain.db-wal= 6: SQLite WAL held the un-checkpointedrows. The extension's per-turn
agent_endconversation logger wrote ~46voice_conversation_*rows during the session (proving the extension ran on every turn).What's genuinely absent is any
voice_core_*row — theremembertool was nevercalled; the verbal ack was hallucinated. UT1/UT2/UT6 conclusions stand.
requests are intent-detection shortcuts inside xiaozhi-server
(
receiveAudioHandle.py:1093-1098and:589-726) that bypass pi's tool loop entirely(the photo turn reached pi as an injected
[You just used your camera...]message).Zero pi tool calls occurred in the entire UAT session.
Why the 4b doesn't call tools (contributing causes, in rough order):
the first character", 1–2 short sentences) actively steers toward an immediate text
reply; a tool call is in tension with "first character is an emoji".
handle it") and
--thinking offremoves deliberation.--no-context-files; only the last user message isforwarded) — pi runs on its default coding-agent system prompt.
models.jsongives qwen3.5:4b a 4096-token context; with 11 tool defs + systemprompt + ~450-token suffix, compaction fired twice on a single fresh turn.
{"content": ...}) —pi_voice.py:88-95forwards theraw JSON instead of unwrapping like
receiveAudioHandle.py:1043does.Also fix (logging-visibility gap, confirmed mechanism).
docker logs dotty-piis emptyby construction: PID 1 is
sleep infinityand each RPC turn is adocker execchild whosestdio goes to PiClient.
--no-sessiondisables pi's own transcripts, andpi_client.py:265logs unknown frames only at DEBUG. Suggested: log
toolCallframes at INFO inPiClient, and/or drop--no-session/tee transcripts.Side finds.
XIAOZHI_HOSTis unset in the dotty-pi container, so the piplay_songtool would fail if it ever were called (
play_song.ts:104); aPiClientError: new_session timed outat 19:25:05 (pre-UAT) shows the RPC channel has reliability wobbles.Expected. Keepable facts write to
brain.db(category=core); hard questions swap toqwen3.6:27b-thinkwith a visible pause.Original log excerpt.
From the filmed UAT session 2026-07-11 (
uat-sessions/2026-07-11/: results.csv, container logs, two video recordings). Investigated and drafted with AI assistance (Claude Fable 5 via Claude Code); verification included read-only live-container probes. Human-reviewed before filing.