Skip to content

Mid-session wake = barge-in, not a reconnect - #10

Merged
jayzuccarelli merged 3 commits into
mainfrom
wake-bargein
Aug 8, 2026
Merged

Mid-session wake = barge-in, not a reconnect#10
jayzuccarelli merged 3 commits into
mainfrom
wake-bargein

Conversation

@jayzuccarelli

@jayzuccarelli jayzuccarelli commented Aug 1, 2026

Copy link
Copy Markdown
Owner

"Hey Mycroft" during an open session used to stop/start the websocket: goodbye cue on every re-wake, reconnect ate the start of the command, and a bare wake left the in-flight reply playing (the mic gate feeds server_vad silence during playback, so it can never auto-cancel).

Firmware (voice_pe_dual.yaml): mid-session wake now calls the previously unused voice_assistant_websocket.interrupt action — cuts local playback, keeps the socket, notifies the broker via the existing {"type":"interrupt"} text frame. Zero new C++. on_stopped's end cue now only fires on real session ends.

Broker: device→broker text frames become control messages (RawPCMSerializer.on_control). On interrupt: explicit response.cancel (guarded; the benign race with response.done is swallowed in VoicePERealtimeService), pipeline interruption flushes the ~2s send-ahead, playback clock rewinds past the VAD release margin so the mic opens immediately, reset_vad clears stale state, hygiene grants a fresh window/budget/grace.

Tests (dev broker on 8766): new --wake suite 3/3 — mid-reply cut leaves 0.2s tail then answers a follow-up; wake while idle answers next turn; bare wake with no follow-up stays silent. Legacy 10/10, --hygiene 3/3.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Wake-word interruptions now work within active voice sessions without requiring reconnection.
    • Interruptions stop ongoing responses, clear pending audio, reset listening state, and resume promptly.
    • Added support for device control messages during a session.
  • Bug Fixes

    • Improved handling of harmless cancellation race conditions.
    • Malformed control messages are safely logged and ignored.
  • Tests

    • Added scenarios covering interruptions during playback, idle periods, and without follow-up speech.

"Hey Mycroft" during an open session used to stop and restart the
websocket: the stop fired on_stopped's end cue (goodbye sound on every
re-wake), the reconnect ate the first words of the command, and a wake
with no follow-up left the in-flight reply playing (server_vad never
hears the user while the mic gate feeds it silence).

Firmware: the wake handler now calls the (previously unused)
voice_assistant_websocket.interrupt action when a session is running —
cuts local playback, keeps the socket, notifies the broker with the
existing {"type":"interrupt"} text frame. No new C++.

Broker: device->broker text frames become control messages. On
"interrupt": explicitly cancel any in-flight response (server_vad can't
auto-cancel on speech it never heard; the benign cancel/response.done
race is swallowed), flush the pipeline, rewind the playback clock past
the VAD release margin so the mic opens for the command immediately,
clear stale VAD state, and grant a fresh hygiene window.

Harness: new --wake set (cut mid-reply, wake while idle, bare wake with
no follow-up). Wake 3/3, legacy 10/10, hygiene 3/3 against a dev broker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0cc0c36b-cbba-4302-8621-f061fbd293cf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds JSON device control frames for wake interrupts. The broker cancels active responses, resets playback and VAD state, and preserves the websocket session. Firmware and harness scenarios support and validate the new flow.

Changes

Wake interruption flow

Layer / File(s) Summary
Control frame parsing and cancellation errors
broker/realtime_broker/serializer.py, broker/realtime_broker/agent.py
RawPCMSerializer parses JSON control frames and invokes on_control. Malformed JSON is logged and dropped. The broker suppresses only response_cancel_not_active.
Broker interrupt handling
broker/realtime_broker/server.py
interrupt controls cancel active responses, reset playback and VAD state, restart turn hygiene, and reopen the playback gate.
Firmware websocket wake behavior
firmware/voice_pe_dual.yaml
Active sessions send a websocket interrupt and keep the connection open. Inactive sessions retain the existing wake sound, delay, and websocket start flow.
Wake interruption scenarios
broker/tools/harness.py
The harness adds wake scenarios for playback, idle listening, and no follow-up speech. The --wake option selects these scenarios.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Device
  participant RawPCMSerializer
  participant VoicePERealtimeService
  participant Pipeline
  Device->>RawPCMSerializer: Send {"type":"interrupt"}
  RawPCMSerializer->>VoicePERealtimeService: Dispatch parsed control data
  VoicePERealtimeService->>Pipeline: Cancel active response
  Pipeline-->>VoicePERealtimeService: Finish interruption processing
  VoicePERealtimeService->>VoicePERealtimeService: Reset playback and VAD state
  VoicePERealtimeService-->>Device: Keep websocket session active
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change from reconnect-based wake handling to mid-session barge-in interruption.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wake-bargein

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jayzuccarelli

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
broker/tools/harness.py (1)

534-534: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the scenario-set selection to one helper.

Line 534 and Line 580 repeat the same precedence rule. If you add a fourth mode and update only one site, --only validates against one set while run executes another. The failure is silent.

The rule also drops --hygiene without warning when a user passes --wake --hygiene together.

Define the selection once and reject conflicting mode flags.

♻️ Proposed refactor

Add a helper near the registries:

def _scenario_set(hygiene: bool, wake: bool) -> dict:
    if wake and hygiene:
        raise SystemExit("--wake and --hygiene are mutually exclusive")
    if wake:
        return WAKE_SCENARIOS
    if hygiene:
        return HYGIENE_SCENARIOS
    return SCENARIOS

Use it in run:

-    scenario_set = WAKE_SCENARIOS if wake else HYGIENE_SCENARIOS if hygiene else SCENARIOS
+    scenario_set = _scenario_set(hygiene, wake)

Use it in main:

     if "--only" in sys.argv:
         only = sys.argv[sys.argv.index("--only") + 1]
-        valid = WAKE_SCENARIOS if wake else HYGIENE_SCENARIOS if hygiene else SCENARIOS
+        valid = _scenario_set(hygiene, wake)
         if only not in valid:
             raise SystemExit(f"unknown scenario {only!r}; one of: {', '.join(valid)}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@broker/tools/harness.py` at line 534, Add a shared _scenario_set(hygiene,
wake) helper near the scenario registries that rejects both flags together and
returns WAKE_SCENARIOS, HYGIENE_SCENARIOS, or SCENARIOS in the existing
precedence order. Replace the duplicated selection logic in run and main so
validation and execution always use the same scenario set.
broker/realtime_broker/serializer.py (1)

36-36: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Widen deserialize to allow the passed str path.

deserialize accepts both str control frames and bytes audio frames, but RawPCMSerializer.deserialize is annotated with message: bytes; use the base str | bytes span so type checkers can see the control-frame path.
[maintainability và_code_quality]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@broker/realtime_broker/serializer.py` at line 36, Update
RawPCMSerializer.deserialize to annotate its message parameter as str | bytes,
matching the base deserialize contract and supporting both control-frame strings
and audio-frame bytes. Preserve the existing return type and deserialization
behavior.
broker/realtime_broker/agent.py (1)

50-58: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Confirm the private upstream hook against the pinned Pipecat installation.

_handle_evt_error is a private method with no import/attribute guard; changing the base class to remove or rename it makes this override a no-op. Add a startup guard that either checks OpenAIRealtimeLLMService._handle_evt_error or imports the exact class from the installed Pipecat wheel.

The patched code already guards the evt.error.code chain safely; the remaining work is catching future upstream method renames explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@broker/realtime_broker/agent.py` around lines 50 - 58, Add startup-time
validation for the private upstream hook used by _handle_evt_error, confirming
that OpenAIRealtimeLLMService exposes _handle_evt_error in the pinned Pipecat
installation. Fail explicitly with a clear error if the method is absent or
renamed, while preserving the existing guarded event handling and superclass
delegation.
broker/realtime_broker/server.py (1)

672-678: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Avoid relying on private Pipecat response state.

_current_assistant_response is a private implementation detail; use the public task-state APIs if available, or use getattr(service, "_current_assistant_response", None) so an internal rename does not crash every mid-session wake.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@broker/realtime_broker/server.py` around lines 672 - 678, Update the
response-cancellation guard in the wake handling flow to avoid direct access to
private _current_assistant_response state. Prefer the service’s public
task-state API if available; otherwise retrieve the private attribute safely
with getattr and a None default, preserving cancellation and interruption
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@broker/realtime_broker/serializer.py`:
- Around line 36-45: Update deserialize to dispatch control frames to on_control
only when json.loads returns a dictionary; treat all other valid JSON values as
malformed and return None. Wrap the awaited on_control call in exception
handling so handler failures are logged and contained within deserialize,
preventing them from propagating into the transport receive loop.

---

Nitpick comments:
In `@broker/realtime_broker/agent.py`:
- Around line 50-58: Add startup-time validation for the private upstream hook
used by _handle_evt_error, confirming that OpenAIRealtimeLLMService exposes
_handle_evt_error in the pinned Pipecat installation. Fail explicitly with a
clear error if the method is absent or renamed, while preserving the existing
guarded event handling and superclass delegation.

In `@broker/realtime_broker/serializer.py`:
- Line 36: Update RawPCMSerializer.deserialize to annotate its message parameter
as str | bytes, matching the base deserialize contract and supporting both
control-frame strings and audio-frame bytes. Preserve the existing return type
and deserialization behavior.

In `@broker/realtime_broker/server.py`:
- Around line 672-678: Update the response-cancellation guard in the wake
handling flow to avoid direct access to private _current_assistant_response
state. Prefer the service’s public task-state API if available; otherwise
retrieve the private attribute safely with getattr and a None default,
preserving cancellation and interruption behavior.

In `@broker/tools/harness.py`:
- Line 534: Add a shared _scenario_set(hygiene, wake) helper near the scenario
registries that rejects both flags together and returns WAKE_SCENARIOS,
HYGIENE_SCENARIOS, or SCENARIOS in the existing precedence order. Replace the
duplicated selection logic in run and main so validation and execution always
use the same scenario set.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a6f2a1e-179f-410f-8da8-979398219aaf

📥 Commits

Reviewing files that changed from the base of the PR and between b7381cc and 47b0510.

📒 Files selected for processing (5)
  • broker/realtime_broker/agent.py
  • broker/realtime_broker/serializer.py
  • broker/realtime_broker/server.py
  • broker/tools/harness.py
  • firmware/voice_pe_dual.yaml

Comment thread broker/realtime_broker/serializer.py Outdated
A text frame of 123/null/[] parses fine but isn't a dict, so the control
handler's msg.get() would raise inside the transport receive loop and
kill the session's audio input. Drop non-object frames and contain
handler exceptions instead.

Addresses CodeRabbit review on #10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jayzuccarelli

Copy link
Copy Markdown
Owner Author

On the nitpicks: deserialize annotation widened to bytes | str in f8236ac. The two private-Pipecat-state notes (_handle_evt_error override, _current_assistant_response) are deliberate: pipecat is pinned to 0.0.97 in requirements.lock and both hooks were verified against that version's source; there is no public API for either. Harness scenario-selection helper: skipped, two call sites in a test tool doesn't earn the abstraction.

CI installs latest ruff unpinned; 0.16.2 flags directives that 0.12-era
ruff required. All changes are lint-mechanical, no behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jayzuccarelli
jayzuccarelli marked this pull request as ready for review August 8, 2026 03:38
@jayzuccarelli
jayzuccarelli merged commit 7855b06 into main Aug 8, 2026
3 checks passed
@jayzuccarelli
jayzuccarelli deleted the wake-bargein branch August 8, 2026 03:38
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.

1 participant