memory: count habits over self facts only, and skip retracted ones
The behaviour profile read the newest 2000 rows of the shared facts table and then discarded everything that was not kind=self, so the length of the window was set by the noisiest writer. mavpoll writes a wg_handshake row every time a peer rehandshakes, about every two minutes per peer, which is enough to reduce 2000 rows to under three days. A weekday habit needs two distinct Tuesdays, so that window can never hold one, and she answered that she knows no habits on a store holding a year of taps. RecentActiveFactsByKind filters kind in SQL, and also drops rows a later row voids along with the void marker itself. The old read counted both a retracted tap and its retraction, so a fact he explicitly took back still shaped what she said he usually does. A correction still counts, because a correction is a value he stands behind. Found in review of #59.
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/kami/maven/internal/morning"
|
||||
"github.com/kami/maven/internal/router"
|
||||
"github.com/kami/maven/internal/rss"
|
||||
"github.com/kami/maven/internal/store"
|
||||
"github.com/kami/maven/internal/weather"
|
||||
)
|
||||
|
||||
@@ -177,9 +178,19 @@ func isRestOfDayQuery(text string) bool {
|
||||
return strings.Contains(s, "дальше") || strings.Contains(s, "next")
|
||||
}
|
||||
|
||||
// habitFactWindow — how many recent facts the behaviour profile is counted
|
||||
// habitFactWindow — how many recent SELF facts the behaviour profile is counted
|
||||
// over. Enough for a season of habits without scanning the whole store on every
|
||||
// question; the profile is recomputed on read, so the bound is the cost control.
|
||||
//
|
||||
// The read is kind-filtered in SQL, and that is the load-bearing part. When this
|
||||
// was a plain recent-facts read the window was a row budget over every writer,
|
||||
// and the machine writers dwarf the taps: mavpoll writes a wg_handshake row
|
||||
// whenever a peer rehandshakes, which is roughly every two minutes per peer, so
|
||||
// 2000 rows was under three days of history. A weekday habit needs
|
||||
// memory.MinHabitDays distinct Tuesdays, which such a window can never hold, so
|
||||
// she answered "по вторникам у меня пока нет ничего постоянного" forever on a
|
||||
// store with a year of taps in it. Self facts come from voice taps, and he does
|
||||
// not tap seven hundred times a day.
|
||||
const habitFactWindow = 2000
|
||||
|
||||
// queryHabits — "что я обычно делаю по вторникам?" (Vikunja #254). Counts the
|
||||
@@ -190,7 +201,7 @@ func (h *reactiveHandler) queryHabits(ctx context.Context, t *queryTurn) (string
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
facts, err := h.api.RecentFacts(ctx, habitFactWindow)
|
||||
facts, err := h.api.RecentActiveFactsByKind(ctx, string(store.KindSelf), habitFactWindow)
|
||||
if err != nil {
|
||||
log.Printf("voice: habits: recent facts: %v", err)
|
||||
return "не получилось посмотреть записи.", true
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/kami/maven/internal/ipc"
|
||||
"github.com/kami/maven/internal/router"
|
||||
"github.com/kami/maven/internal/store"
|
||||
)
|
||||
|
||||
// planAPI answers only DayPlan; every other call is unimplemented, which is
|
||||
@@ -142,17 +143,21 @@ func TestDayPlanSourcePrecedesCalendar(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// habitAPI answers only RecentFacts — the whole input the behaviour profile
|
||||
// needs (Vikunja #254). Nothing is asked of the LLM, so nothing else is wired.
|
||||
// habitAPI answers only the kind-filtered fact read — the whole input the
|
||||
// behaviour profile needs (Vikunja #254). Nothing is asked of the LLM, so
|
||||
// nothing else is wired. RecentFacts is left unimplemented on purpose: the
|
||||
// profile must not read the mixed window, and a caller that does fails here.
|
||||
type habitAPI struct {
|
||||
ipc.UnimplementedCoreAPI
|
||||
facts []ipc.Fact
|
||||
err error
|
||||
calls int
|
||||
kind string
|
||||
}
|
||||
|
||||
func (a *habitAPI) RecentFacts(_ context.Context, _ int) ([]ipc.Fact, error) {
|
||||
func (a *habitAPI) RecentActiveFactsByKind(_ context.Context, kind string, _ int) ([]ipc.Fact, error) {
|
||||
a.calls++
|
||||
a.kind = kind
|
||||
return a.facts, a.err
|
||||
}
|
||||
|
||||
@@ -225,3 +230,23 @@ func TestHabitSourcePrecedesCalendar(t *testing.T) {
|
||||
t.Errorf("habits at %d must come before calendar at %d", habits, cal)
|
||||
}
|
||||
}
|
||||
|
||||
// TestQueryHabitsReadsSelfFactsOnly — the profile window is a budget over rows,
|
||||
// so it must be spent on the rows the profile can use. Reading the mixed table
|
||||
// let one chatty poller (wg_handshake, roughly every two minutes per peer) push
|
||||
// every tap out of the window, and she then reported no habits on a store that
|
||||
// held them.
|
||||
func TestQueryHabitsReadsSelfFactsOnly(t *testing.T) {
|
||||
now := planDay()
|
||||
api := &habitAPI{facts: tuesdayFacts("workout", 19, 4, now)}
|
||||
h := &reactiveHandler{api: api, now: func() time.Time { return now }}
|
||||
|
||||
if _, ok := h.queryHabits(context.Background(), &queryTurn{
|
||||
dec: router.Decision{Intent: router.IntentQuery, Utterance: "что я обычно делаю по вторникам?"},
|
||||
}); !ok {
|
||||
t.Fatal("the habit source must claim a habit question")
|
||||
}
|
||||
if api.kind != string(store.KindSelf) {
|
||||
t.Errorf("profile read kind %q, want %q", api.kind, store.KindSelf)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user