Let mavwaked hear the nudges it was already being sent (V-671)

It wired no PushHandler, and SendRequest discards a push frame when there
is none. That was not a missing feature but a silent one. mavend routes a
nudge to the voice session that spoke most recently, so once mavwaked had
spoken once it WAS that session. PushToMostRecent succeeded, the
dispatcher counted the nudge delivered and stopped rerouting to the away
channels, and mavwaked threw the audio away. He heard nothing, anywhere.

It now connects at startup rather than at the first utterance, because
the dispatcher has to tell "he is not at the machine" from "he is, and
she has nothing to say". The receiver redials on its own clock, since
mavend restarts on every deploy.

A nudge is queued, not played where it arrives. The capture loop picks it
up on the next frame, so the half-duplex gate and barge-in cover it the
way they cover a reply. It resets the VAD first: playback is about to
suppress every frame, and a half-heard sentence would otherwise splice
onto whatever he says next. A nudge arriving while one still waits
replaces it, which is the contract internal/voice states for PushHandler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
This commit is contained in:
2026-08-09 13:45:04 +04:00
parent 1f1e002789
commit 9c7bafd5b1
3 changed files with 155 additions and 1 deletions
+15 -1
View File
@@ -17,6 +17,11 @@
// commands at L0 (no destructive acts), which is what makes an accidental
// trigger safe rather than expensive.
//
// The conn carries both directions. mavwaked sends utterances and receives
// proactive nudges on it, and it is opened at startup rather than at the first
// utterance, because mavend registers a voice session on accept. See nudge.go
// for why a nudge that is not heard is worse than one that is not delivered.
//
// While a reply is playing the capture side is muted (half-duplex): without
// it, Maven's own voice comes back in through the mic and she answers
// herself. -barge-in punches one hole in that gate — sustained energy above
@@ -86,7 +91,8 @@ func run(args []string) error {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
// Voice client — reused across utterances; SendRequest reconnects on error.
// Voice client — one conn carrying both directions. SendRequest reconnects
// on error, and the push receiver redials on its own clock.
vc := voice.Dial(*addr)
defer vc.Close()
@@ -165,6 +171,14 @@ func run(args []string) error {
}
sess := newSession(vad, newAplayPlayer(), &voiceSender{vc: vc}, *lang, barge)
// Listen for nudges alongside capture. Connect eagerly so mavend has a
// voice session before he has said anything: without one, a nudge routed
// to voice finds nobody home and goes to the away channels instead.
if err := vc.Connect(ctx); err != nil {
log.Printf("mavwaked: voice server not reachable yet, retrying in background: %v", err)
}
go runNudgeReceiver(ctx, vc, sess)
return captureLoop(ctx, src, sess)
}