Skip to content

fix: serialize PiVoice RPC turns - #175

Closed
0Downtime wants to merge 1 commit into
BrettKinny:mainfrom
0Downtime:0Downtime/pi-voice-transaction-safety
Closed

0Downtime wants to merge 1 commit into
BrettKinny:mainfrom
0Downtime:0Downtime/pi-voice-transaction-safety

Conversation

@0Downtime

Copy link
Copy Markdown

Summary

  • serialize each complete PiVoice RPC transaction (new_session -> prompt -> agent_end)
  • correlate new_session acknowledgements by request ID
  • surface matching reset failures instead of treating every reset response as success
  • add deterministic regressions for overlapping turns, stale acknowledgements, and rejected resets

Why

PiVoiceLLM owns one long-lived PiClient and one response queue, while a xiaozhi connection uses a five-worker ThreadPoolExecutor and submits conn.chat jobs asynchronously. Two overlapping turns could therefore read from the same ordered Pi RPC stream, allowing one caller to consume another caller's frames.

The reset path also accepted the first new_session response regardless of request ID or success. A stale failed response could complete the reset early and leave the correct acknowledgement queued for the next operation.

A deterministic regression run against unmodified main reproduced the overlap (second turn must wait: two active prompts instead of one). The same test passes with this lock.

Impact

Concurrent or rapidly interrupted voice turns now wait for the active Pi transaction to reach agent_end. Reset acknowledgements cannot be confused with stale responses, and explicit Pi reset failures are logged through the provider's existing fallback path.

Validation

  • ruff check .
  • bridge/Pi Python suite: 164 passed, 11 subtests passed
  • dotty-behaviour suite: 231 passed
  • combined coverage: 70.10% (56% required)
  • PiVoice unit suite: 28 passed across 20 consecutive runs
  • git diff --check

No live-device claim is made; this PR validates the reachable race with a deterministic concurrency harness.

Prepared with OpenAI Codex assistance and reviewed by the contributor before submission.

Correlate new_session acknowledgements by request ID and surface reset failures instead of accepting stale responses.

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

Copy link
Copy Markdown
Owner

Thank you @0Downtime. This landed on main through the release-candidate integration in #185 (squash commit de2efd5): all of the serialization, the request-ID correlation for new_session, and the regression tests are there unchanged. It's credited in the CHANGELOG ("PiVoice RPC turns are serialized (#175, thanks @0Downtime)") and ships in server-v0.2.0-rc.1. Closing because it's already merged that way, not because it was rejected.

— Brett, via Claude Opus 5.5 (AI-assisted)

@BrettKinny BrettKinny closed this Oct 28, 2026
BrettKinny added a commit that referenced this pull request Nov 1, 2026
…voice model (#105, #181) (#204)

* fix(pi_voice): skip queued turns when a newer utterance arrived (#105)

With the #175 turn lock, room-noise turns queue behind a running reply and
are answered late, in order, after the person has moved on. Number each
turn per session before it waits for the lock and drop any turn that was
overtaken by a later one. Turns with no user words never supersede a real one.

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

* feat(asr): close the session after 3 noise-like turns in a row (#105)

A mic left open in a busy room kept feeding TV and podcast audio to the
LLM. Level cannot separate that from a quiet child, so count Whisper's own
confidence signals (empty/dropped transcript, no_speech >= 0.45, avg_logprob
< -1.0) and end the session through the same path as the no-voice timeout
when three come in a row. Any real turn resets the run; the setting is
configurable and 0 disables it.

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

* feat(pi_voice): pre-warm the voice model when a device connects (#181)

The first reply after an idle spell took ~12 s because llama-swap had
unloaded the 4B, with the mic open and anything said read as a barge-in.
The device WebSocket opening is the earliest signal, so send a one-token
request then. Rate limited, fire-and-forget, and a failure is only logged.
llama-swap is shared with other services, so its TTL is left alone.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants