mavend: record which channel a quiet toggle arrived on

resolveQuietToggle runs inside runTurn, so mavweb /api/chat and telegram reach
it as well as the microphone. Every toggle was written with Source "tap:voice"
regardless, which left the facts table claiming a mic flipped a setting nobody
spoke to. This is the one function whose own doc comment calls it a
network-reachable way to change a daemon-wide setting, and provenance is the
first column read when asking why quiet mode is on.

runTurn now takes the channel it was entered from and the toggle writes it:
"tap:voice" from HandlePushToTalk, "tap:text" from handleText.

Found in review of #53.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
kami
2026-08-01 14:05:17 +04:00
parent b2eb08bb51
commit 4757ff6d7b
4 changed files with 46 additions and 8 deletions
+16 -4
View File
@@ -172,7 +172,7 @@ func (h *reactiveHandler) HandlePushToTalk(ctx context.Context, req voice.PushTo
// 2-5. the shared turn pipeline (confirm → clarify → route → dialogue →
// action → replier), identical to the text path.
replyText := h.runTurn(ctx, text)
replyText := h.runTurn(ctx, text, sourceVoice)
// 6. tts — synthesise the reply text; return to the voice server which
// ships it back on the conn.
@@ -184,9 +184,21 @@ func (h *reactiveHandler) HandlePushToTalk(ctx context.Context, req voice.PushTo
// HandlePushToTalk so text channels share the same routing logic.
func (h *reactiveHandler) handleText(ctx context.Context, text string) string {
log.Printf("voice: handleText: %q", text)
return h.runTurn(ctx, text)
return h.runTurn(ctx, text, sourceText)
}
// turnSource — which channel this utterance arrived on, in the same provenance
// vocabulary facts use (internal/event). It is threaded through runTurn because
// a turn can write a fact, and a fact that lies about where it came from is
// worse than no fact: provenance is the first column read when asking why a
// daemon-wide setting is the way it is.
type turnSource string
const (
sourceVoice turnSource = "tap:voice" // HandlePushToTalk, a real microphone
sourceText turnSource = "tap:text" // handleText: mavweb /api/chat, telegram
)
// runTurn — the reactive turn pipeline shared by the voice and text entry
// points: expired-clarify notice → confirm answer → clarify answer → quiet
// toggle → route → dialogue merge → clarify question → action → replier.
@@ -194,7 +206,7 @@ func (h *reactiveHandler) handleText(ctx context.Context, text string) string {
// path wraps it in stt/tts, the text path returns it as-is.
//
// The ordering is load-bearing — see the step comments.
func (h *reactiveHandler) runTurn(ctx context.Context, text string) string {
func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSource) string {
// 1. expired clarify — a question was parked but its TTL ran out, so the
// request behind it is gone. Say that out loud (see clarify.go) and carry
// on: these words are still routed as a fresh utterance below, with the
@@ -230,7 +242,7 @@ func (h *reactiveHandler) runTurn(ctx context.Context, text string) string {
// "тихий режим" / "quiet on" would route through the classifier
// unreliably (it's a command, not a free-form query), so we match it
// before routing. Same pattern as the confirm turn above.
if reply, handled := h.resolveQuietToggle(ctx, text); handled {
if reply, handled := h.resolveQuietToggle(ctx, text, src); handled {
return withNotice(expiredNotice, reply)
}