From 0feb8d3dbd96e39bdbc1ded34f7e879574460b73 Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 6 Aug 2026 02:05:01 +0400 Subject: [PATCH 1/2] voicewire: fix drifted seed comments, dedupe dialogue TTL (V-581) buildRouter's third bullet described a hardcoded 6-example bootstrap set that predates seedClassifier's file-based loader; seedClassifier's own comment named 5 seed files where there are 7 (chat.txt and system.txt were missing). Also named the repeated 2*time.Minute dialogue session TTL literal as dialogueSessionTTL so the two call sites can't drift apart. --- cmd/mavend/voicewire.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cmd/mavend/voicewire.go b/cmd/mavend/voicewire.go index e005bed..7fb9772 100644 --- a/cmd/mavend/voicewire.go +++ b/cmd/mavend/voicewire.go @@ -236,21 +236,22 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem memStore = memory.NewInMemoryStore() } - // ----- dialogue (multi-turn slot carry-over; 2-min follow-up window) ----- + // ----- dialogue (multi-turn slot carry-over; dialogueSessionTTL follow-up window) ----- // Store-backed when the daemon passes a store, so a restart mid-conversation // keeps the thread (Vikunja #363). Sessions past their TTL are dropped on // load, never revived. Clarify's parked question stays in memory only, and // that is a decision rather than an omission (Vikunja #385, docs/design.md): // a restart expires it, so the thread comes back and the open question does // not. + const dialogueSessionTTL = 2 * time.Minute var dialogueSessions *dialogue.SessionStore if dataStore != nil { - dialogueSessions = dialogue.NewPersistentSessionStore(2*time.Minute, dataStore) + dialogueSessions = dialogue.NewPersistentSessionStore(dialogueSessionTTL, dataStore) if err := dialogueSessions.Load(context.Background(), time.Now()); err != nil { log.Printf("dialogue: load saved sessions: %v", err) } } else { - dialogueSessions = dialogue.NewSessionStore(2 * time.Minute) + dialogueSessions = dialogue.NewSessionStore(dialogueSessionTTL) } clarifyStore := dialogue.NewClarifyStore(clarifyTTL) timeParser := router.NewPythonDateParser() @@ -376,9 +377,9 @@ func pickLLMRouter(enabled bool, c router.Completer) *router.LLMRouter { // - The embedder is provided by wireVoice: HashEmbedder (floor) when no // embedder config is present, or the ONNX multilingual model when // configured — same interface, one constructor change. -// - 6 bootstrap examples covering the 5 intents + one compound-capture -// placeholder. Spec calls for ~10 per intent at production; this is the -// bootstrapping floor swapped by tuning the seed set later. +// - The classifier is floored by seedClassifier, which loads one file per +// intent from seedDir (models/seeds/.txt) — see seedClassifier +// below for the current intent list and file names. // - Threshold is from voice.router_threshold config (default 0.55). func buildRouter(emb router.Embedder, acts router.ActMatcher, threshold float64, llmR *router.LLMRouter) *router.Router { cls := router.NewClassifier(emb) @@ -461,9 +462,10 @@ func seedPath() string { // seedClassifier floors the embedded examples so the cold-boot path // doesn't return ErrNoIntents. Loads examples from seedDir — one file per -// intent (act.txt, reminder.txt, fact.txt, note.txt, query.txt). When the -// classifier can't decide it falls through to Clarify — the last-resort -// path asks the user to rephrase rather than guessing wrong. +// intent (act.txt, reminder.txt, fact.txt, note.txt, query.txt, chat.txt, +// system.txt). When the classifier can't decide it falls through to +// Clarify — the last-resort path asks the user to rephrase rather than +// guessing wrong. func seedClassifier(c *router.Classifier) { intents := []router.Intent{ router.IntentAct, From b6680398c31239220543179d02a6e7074004030b Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 6 Aug 2026 02:08:05 +0400 Subject: [PATCH 2/2] tick: dedupe rule-by-name map building (V-581) stopFinishedAlarms and repeatableRules each built their own map[string]rule (one keyed to loop.Rule, one to bool) from t.rules on every call. Factored into rulesByName(), one map[string]loop.Rule both callers read. --- cmd/mavend/tick.go | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/cmd/mavend/tick.go b/cmd/mavend/tick.go index 9de2b74..76b0911 100644 --- a/cmd/mavend/tick.go +++ b/cmd/mavend/tick.go @@ -319,10 +319,7 @@ func (t *tickLoop) stopFinishedAlarms(ctx context.Context, keys []string, state if len(keys) == 0 { return nil } - byName := make(map[string]loop.Rule, len(t.rules)) - for _, r := range t.rules { - byName[r.Name] = r - } + byName := t.rulesByName() live := keys[:0:0] for _, key := range keys { outcome := "" @@ -381,19 +378,27 @@ func (t *tickLoop) repeatableRules(keys []string) []string { if len(keys) == 0 { return nil } - wired := make(map[string]bool, len(t.rules)) - for _, r := range t.rules { - wired[r.Name] = true - } + wired := t.rulesByName() out := keys[:0:0] for _, k := range keys { - if wired[k] { + if _, ok := wired[k]; ok { out = append(out, k) } } return out } +// rulesByName indexes the wired rule set by name, for the two lookups above +// that only care whether a key is still wired (repeatableRules) or need the +// rule itself (stopFinishedAlarms). +func (t *tickLoop) rulesByName() map[string]loop.Rule { + byName := make(map[string]loop.Rule, len(t.rules)) + for _, r := range t.rules { + byName[r.Name] = r + } + return byName +} + // cachePhrase keeps the latest phrased nudge per rule for the sev4-repeat // path. writing under a mutex; the repeat path reads under the same. the // cache is bounded by the rule count (≤ ~30 per spec) so eviction is not a