Compare commits

...

3 Commits

Author SHA1 Message Date
claude e488ee2285 Merge the tick and voicewire sweep (#226)
Comment drift and one duplication, no behaviour change. buildRouter's doc
described a hardcoded bootstrap seeding scheme that no longer exists, and
seedClassifier's own comment named five seed files where the code seeds seven.
The dialogue session TTL was written twice, once per branch of one if/else.
stopFinishedAlarms and repeatableRules each rebuilt the same rules-by-name map.

(V-581)
2026-08-06 02:08:27 +04:00
claude b6680398c3 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.
2026-08-06 02:08:05 +04:00
claude 0feb8d3dbd 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.
2026-08-06 02:05:01 +04:00
2 changed files with 25 additions and 18 deletions
+14 -9
View File
@@ -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
+11 -9
View File
@@ -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/<intent>.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,