From 3f98a99f44da873876405e4305e22856ca99b206 Mon Sep 17 00:00:00 2001 From: kami Date: Sat, 1 Aug 2026 22:21:28 +0400 Subject: [PATCH] continuation: only an ellipsis may widen the keyword match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deployed check, second round: "привет" after "какой сегодня день" answered with the date. followUpMerge fills an empty Text from the previous same-intent turn, so the topic-widening added a minute earlier was reading an inherited topic on turns that had nothing to do with it. Decision gains Continued, set only by continuation.go and never by the router. replySystem widens on that and nothing else, so an inherited Text is back to being invisible. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX --- cmd/mavend/continuation.go | 1 + cmd/mavend/continuation_test.go | 22 ++++++++++++++++++++++ cmd/mavend/voice.go | 12 +++++++++--- internal/router/intent.go | 7 +++++++ 4 files changed, 39 insertions(+), 3 deletions(-) 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 }