From 17964d116289db188d578218646f692da5c06f21 Mon Sep 17 00:00:00 2001 From: kami Date: Sat, 1 Aug 2026 22:57:08 +0400 Subject: [PATCH] dialogue: keep the remembered topic on the latest turn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rememberTurn runs after followUpMerge, which has already inherited a Text slot from the previous same-intent turn, so the fill-if-empty rule pinned the first topic of a run of query turns and never released it. "во сколько у меня встреча", then "какие у меня планы", then "а завтра?" continued the meeting — two turns stale. Overwrite for system and query, where Text is a topic and not a payload. A continuation is the exception and keeps what it inherited: its own utterance is the ellipsis, and the topic it carries is the real one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX --- cmd/mavend/clarify.go | 11 ++++++++++- cmd/mavend/continuation_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/cmd/mavend/clarify.go b/cmd/mavend/clarify.go index 81d271b..591ff9a 100644 --- a/cmd/mavend/clarify.go +++ b/cmd/mavend/clarify.go @@ -353,8 +353,17 @@ func (h *reactiveHandler) rememberTurn(prev *dialogue.Session, dec router.Decisi // which knows the day but not what was asked ABOUT, so keep the raw // utterance where continuation.go can find it. Only these two intents: // everywhere else Text is a payload and must stay what the router put in. + // + // Overwritten, not filled: rememberTurn runs AFTER followUpMerge, which + // has already inherited a Text from the previous same-intent turn, so a + // fill-if-empty rule keeps the OLD topic for ever. Seen on the deployed + // daemon 01-08-2026 — "во сколько у меня встреча" then "какие у меня + // планы" then "а завтра?" continued the meeting, two turns stale. + // + // A continuation is the exception and keeps what it inherited: its + // utterance is the ellipsis, and the topic it carries is the real one. slots := toDialogueSlots(dec.Slots) - if slots.Text == "" && (dec.Intent == router.IntentSystem || dec.Intent == router.IntentQuery) { + if !dec.Continued && (dec.Intent == router.IntentSystem || dec.Intent == router.IntentQuery) { slots.Text = dec.Utterance } h.dialogueSessions.Put(voiceDialogueID, &dialogue.Session{ diff --git a/cmd/mavend/continuation_test.go b/cmd/mavend/continuation_test.go index 9ae8b26..a087eca 100644 --- a/cmd/mavend/continuation_test.go +++ b/cmd/mavend/continuation_test.go @@ -138,3 +138,32 @@ func TestReplySystemIgnoresAnInheritedTopic(t *testing.T) { t.Fatalf("replySystem refused a real continuation") } } + +// TestRememberTurnRefreshesTheTopic — rememberTurn runs after followUpMerge, +// which has already inherited a Text from the previous same-intent turn. A +// fill-if-empty rule therefore pins the FIRST topic of a run of query turns +// and never lets go, so a later "а завтра?" continues a question two turns +// old. Seen on the deployed daemon, 01-08-2026. +func TestRememberTurnRefreshesTheTopic(t *testing.T) { + h := &reactiveHandler{ + now: func() time.Time { return contNow }, + dialogueSessions: dialogue.NewSessionStore(2 * time.Minute), + } + h.rememberTurn(nil, router.Decision{ + Intent: router.IntentQuery, Utterance: "во сколько у меня встреча", + }, contNow) + // The second turn arrives with the first turn's Text already merged in. + prev := h.dialogueSessions.Get(voiceDialogueID, contNow) + h.rememberTurn(prev, router.Decision{ + Intent: router.IntentQuery, + Utterance: "какие у меня планы", + Slots: router.Slots{Text: "во сколько у меня встреча"}, + }, contNow) + got := h.dialogueSessions.Get(voiceDialogueID, contNow) + if got == nil { + t.Fatal("no session") + } + if got.Slots.Text != "какие у меня планы" { + t.Fatalf("topic = %q, want the latest turn's", got.Slots.Text) + } +}