store: keep what she read out of what he said

Nothing at read time told a feed item or a crawled page apart from his own
notes. QueryNotes ranked every note by cosine and the notes answer handed the
nearest five to the phraser, so "что я говорил про переезд" could be answered
out of a stranger's web page, prefixed with "вот что я нашла: ". Recall now
excludes the read sources, rss: and crawl:, and the list is one place.

The feed answer needed a different read as a result, and it needed one anyway:
it scanned the last 200 notes of any source, so a busy day of voice notes pushed
the newest headline out of the window and she said "в лентах пока ничего
нового" while the poller was working fine. RecentNotesFromSource asks for feed
notes by source, so the window holds 200 of them.

Found in review of #66 and #67.
This commit is contained in:
kami
2026-08-01 14:24:40 +04:00
parent 694d9e4e45
commit 57161fb762
10 changed files with 180 additions and 10 deletions
+5 -5
View File
@@ -206,7 +206,7 @@ func (h *reactiveHandler) queryHabits(ctx context.Context, t *queryTurn) (string
return profile.FormatOverallRU(), true
}
// feedNoteWindow — how many recent notes are scanned for feed items, and
// feedNoteWindow — how many recent FEED notes are scanned, and
// feedReadOut — how many headlines she actually reads back. She summarises the
// top of the pile, she does not recite a river.
const (
@@ -231,16 +231,16 @@ func (h *reactiveHandler) queryFeeds(ctx context.Context, t *queryTurn) (string,
// news bulletin.
return "я пока не читаю ленты — они не настроены.", true
}
notes, err := h.api.RecentNotes(ctx, feedNoteWindow)
// By source, not the last 200 notes of any kind: a busy day of voice notes
// used to push the newest headline out of the window, and she answered "в
// лентах пока ничего нового" while the poller was working fine.
notes, err := h.api.RecentNotesFromSource(ctx, rss.SourcePrefix, feedNoteWindow)
if err != nil {
log.Printf("voice: feeds: recent notes: %v", err)
return "не получилось посмотреть ленты.", true
}
var picked []string
for _, n := range notes {
if !strings.HasPrefix(n.Source, rss.SourcePrefix) {
continue
}
if !router.CategoryMatches(rss.NoteCategory(n.Text), q.Category) {
continue
}
+21 -2
View File
@@ -88,12 +88,12 @@ func TestQueryFeedsByCategory(t *testing.T) {
// answered by the model inventing a bulletin.
func TestQueryFeedsOffAndEmptyDiffer(t *testing.T) {
off := buildFeedHandler(t, false)
reply, ok := askFeeds(t, off, "что нового?")
reply, ok := askFeeds(t, off, "что нового в лентах?")
if !ok || !strings.Contains(reply, "не настроены") {
t.Fatalf("feeds off: reply = %q, ok = %v", reply, ok)
}
on := buildFeedHandler(t, true)
reply, ok = askFeeds(t, on, "что нового?")
reply, ok = askFeeds(t, on, "что нового в лентах?")
if !ok || !strings.Contains(reply, "ничего нового") {
t.Fatalf("feeds on but empty: reply = %q, ok = %v", reply, ok)
}
@@ -104,6 +104,25 @@ func TestQueryFeedsPassesOnANonFeedQuestion(t *testing.T) {
if reply, ok := askFeeds(t, h, "напомни полить цветы"); ok {
t.Fatalf("claimed an unrelated question with %q", reply)
}
// The bare greeting is not a request for headlines. It used to be answered
// with a configuration status.
if reply, ok := askFeeds(t, h, "что нового?"); ok {
t.Fatalf("claimed a greeting with %q", reply)
}
}
// A busy day of his own notes must not push the newest headline out of the
// window the feed answer scans.
func TestQueryFeedsIsNotCrowdedOutByHisOwnNotes(t *testing.T) {
notes := []ipc.Note{{Text: "Релиз ядра [технологии]", Source: "rss:habr"}}
for i := 0; i < feedNoteWindow+10; i++ {
notes = append(notes, ipc.Note{Text: "мысль вслух", Source: "tap:voice"})
}
h := buildFeedHandler(t, true, notes...)
reply, ok := askFeeds(t, h, "что нового в лентах?")
if !ok || !strings.Contains(reply, "ядра") {
t.Fatalf("reply = %q, ok = %v; the headline fell out of the window", reply, ok)
}
}
// The mark is what stops a restart from re-noting yesterday's headlines, so the