diff --git a/cmd/mavend/continuation.go b/cmd/mavend/continuation.go index bbe0b1a..9beda47 100644 --- a/cmd/mavend/continuation.go +++ b/cmd/mavend/continuation.go @@ -82,6 +82,7 @@ func continuationDecision(prev *dialogue.Session, text string, now time.Time) (r Intent: router.Intent(prev.Intent), Confidence: 1.0, Stage: 0, + Continued: true, Slots: router.Slots{ Key: prev.Slots.Key, HasKey: prev.Slots.HasKey, diff --git a/cmd/mavend/continuation_test.go b/cmd/mavend/continuation_test.go index f4816f9..9ae8b26 100644 --- a/cmd/mavend/continuation_test.go +++ b/cmd/mavend/continuation_test.go @@ -116,3 +116,25 @@ func TestContinuationCarriesTheTopic(t *testing.T) { t.Fatalf("Slots.Text = %q, want the previous turn's topic", dec.Slots.Text) } } + +// TestReplySystemIgnoresAnInheritedTopic — the regression the deployed daemon +// showed on 01-08-2026: followUpMerge fills an empty Text from the previous +// same-intent turn, so a plain "привет" after "какой сегодня день" arrived at +// replySystem carrying the old topic and was answered with the date. Only a +// continuation may widen the keyword match. +func TestReplySystemIgnoresAnInheritedTopic(t *testing.T) { + h := &reactiveHandler{now: func() time.Time { return contNow }} + inherited := router.Decision{ + Utterance: "привет", + Intent: router.IntentSystem, + Slots: router.Slots{Text: "какой сегодня день"}, + } + if got := h.replySystem(nil, inherited); got != "пока не умею отвечать на этот вопрос." { + t.Fatalf("replySystem answered %q on an inherited topic", got) + } + cont := inherited + cont.Utterance, cont.Continued = "а завтра?", true + if got := h.replySystem(nil, cont); got == "пока не умею отвечать на этот вопрос." { + t.Fatalf("replySystem refused a real continuation") + } +} diff --git a/cmd/mavend/voice.go b/cmd/mavend/voice.go index 138b0d8..e449815 100644 --- a/cmd/mavend/voice.go +++ b/cmd/mavend/voice.go @@ -374,10 +374,16 @@ func (h *reactiveHandler) replySystem(ctx context.Context, dec router.Decision) // lives in the previous turn, which continuation.go copied into // Slots.Text. Dates keep parsing from the utterance — that is the part // the ellipsis actually restates — and only the keyword match widens. - // On an ordinary turn Slots.Text is the utterance, so nothing changes. + // + // Gated on Continued, and that gate is load-bearing. followUpMerge fills + // an empty Text from the previous same-intent turn, so without it a plain + // "привет" after "какой сегодня день" inherited the old topic and got + // answered with the date. Seen on the deployed daemon, 01-08-2026. topic := u - if t := strings.ToLower(dec.Slots.Text); t != "" && t != u { - topic = u + " " + t + if dec.Continued { + if t := strings.ToLower(dec.Slots.Text); t != "" && t != u { + topic = u + " " + t + } } // stage-0 grammars catch the exact time/date patterns, but duration diff --git a/internal/router/intent.go b/internal/router/intent.go index 3ecc4ef..758c042 100644 --- a/internal/router/intent.go +++ b/internal/router/intent.go @@ -104,4 +104,11 @@ type Decision struct { Confidence float64 // 1.0 for stage-0; classifier cosine similarity for 1+ Slots Slots Clarify bool // stage 3: below threshold — ask, don't guess + + // Continued — this decision was rebuilt from the previous turn rather + // than routed, because the utterance was an ellipsis ("а завтра?"). + // Handlers use it to know that Slots.Text is the PREVIOUS turn's topic + // and not something the current utterance said. Nothing in the router + // sets it; the daemon's continuation path does. + Continued bool }