From b25377b6cac4165ba3e156846ebf34984ac074a6 Mon Sep 17 00:00:00 2001 From: kami Date: Mon, 6 Jul 2026 12:15:18 +0400 Subject: [PATCH] maven: recall long-term memory in the query path (task 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 7 inserted note embeddings into the memory Store but nothing read them back, and facts weren't indexed at all. Complete the read side: - Facts are now embedded and inserted into memStore on capture (best-effort, never fails the fact write) — the notes table can't answer fact questions ("когда я пил воду?"), so memStore is their only recall path. - Insert meta now carries text/ts/type so a Search hit is self-describing. - IntentQuery consults memStore.Search after notes-RAG misses and before the general-knowledge phraser fallback (bestRecall, unit-tested). Strictly additive: it only runs once the notes path has already given up, so it can't regress existing recall. Note hits here overlap notes-RAG by design; the payoff is fact recall and a real read seam for a future persistent backend. Co-Authored-By: Claude Opus 4.8 --- cmd/mavend/recall.go | 25 ++++++++++++++++++++++ cmd/mavend/recall_test.go | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 cmd/mavend/recall.go create mode 100644 cmd/mavend/recall_test.go diff --git a/cmd/mavend/recall.go b/cmd/mavend/recall.go new file mode 100644 index 0000000..0027adf --- /dev/null +++ b/cmd/mavend/recall.go @@ -0,0 +1,25 @@ +package main + +import "github.com/kami/maven/internal/memory" + +// bestRecall is the read side of the long-term memory store: the top hit's +// stored text when it clears the confidence gate. This recalls across BOTH +// notes and facts (facts aren't in the notes table, so this is the only path +// that can answer "when did I last …?" from a captured fact). A note hit here +// is redundant with the notes-RAG path — by design; the two indexes can diverge +// once the backend is swapped for a persistent/external store. ok=false when +// there's no hit above the threshold or the hit carries no text. +func bestRecall(results []memory.Result, min float64) (string, bool) { + if len(results) == 0 { + return "", false + } + top := results[0] + if top.Score < min { + return "", false + } + text := top.Meta["text"] + if text == "" { + return "", false + } + return text, true +} diff --git a/cmd/mavend/recall_test.go b/cmd/mavend/recall_test.go new file mode 100644 index 0000000..d527a50 --- /dev/null +++ b/cmd/mavend/recall_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "testing" + + "github.com/kami/maven/internal/memory" +) + +func TestBestRecall(t *testing.T) { + const min = 0.55 + + t.Run("empty results", func(t *testing.T) { + if _, ok := bestRecall(nil, min); ok { + t.Error("empty results returned ok") + } + }) + + t.Run("top below threshold", func(t *testing.T) { + res := []memory.Result{{Score: 0.4, Meta: map[string]string{"text": "выпил воды"}}} + if _, ok := bestRecall(res, min); ok { + t.Error("below-threshold hit returned ok") + } + }) + + t.Run("hit without text meta", func(t *testing.T) { + res := []memory.Result{{Score: 0.9, Meta: map[string]string{"type": "fact"}}} + if _, ok := bestRecall(res, min); ok { + t.Error("textless hit returned ok") + } + }) + + t.Run("clearing hit returns its text", func(t *testing.T) { + res := []memory.Result{ + {Score: 0.82, Meta: map[string]string{"text": "выпил воды в три часа", "type": "fact"}}, + {Score: 0.60, Meta: map[string]string{"text": "другое"}}, + } + got, ok := bestRecall(res, min) + if !ok { + t.Fatal("clearing hit not returned") + } + if got != "выпил воды в три часа" { + t.Errorf("wrong text: %q", got) + } + }) +}