Bug: spoken task capture is dead — the router calls the marker an act, and capture only rides the note intent #142

Merged
claude merged 5 commits from task/467-bug-spoken-task-capture-is-dead-the-rout into master 2026-08-04 18:24:32 +02:00
4 changed files with 60 additions and 0 deletions
Showing only changes of commit 77206f298e - Show all commits
+3
View File
@@ -385,6 +385,9 @@ func buildRouter(emb router.Embedder, acts router.ActMatcher, threshold float64,
// Same reason as the agenda rules, for the feeds: "что нового в лентах?"
// routed system and answered "пока не умею" (Vikunja #474).
grammars = append(grammars, router.FeedQueryGrammar())
// The list side of the same exposure: a phrasing with no possessive in it
// ("список дел") routed system and never reached queryTasks (Vikunja #467).
grammars = append(grammars, router.TaskListGrammar())
grammars = append(grammars, router.ReminderGrammar())
// Last, and it matches any utterance shape — its Build is the filter. An
// explicit capture marker beats the model, which called it an act and
+4
View File
@@ -237,7 +237,11 @@ func newBaselineRouter(t *testing.T, emb router.Embedder, llmR *router.LLMRouter
// anything while its grammar set is the daemon's grammar set.
grammars = append(grammars, router.AgendaQueryGrammars()...)
grammars = append(grammars, router.FeedQueryGrammar())
// The list side of the same exposure: a phrasing with no possessive in it
// ("список дел") routed system and never reached queryTasks (Vikunja #467).
grammars = append(grammars, router.TaskListGrammar())
grammars = append(grammars, router.ReminderGrammar())
grammars = append(grammars, router.TaskCaptureGrammar())
return router.New(router.Config{
Grammars: grammars,
Classifier: cls,
+29
View File
@@ -248,3 +248,32 @@ func TaskCaptureGrammar() Grammar {
},
}
}
// TaskListGrammar — stage 0 for "какие у меня задачи", "список дел", "что мне
// нужно сделать" (Vikunja #467).
//
// The same exposure the capture marker had, pointed the other way. IsTaskListQuery
// is a deterministic lookup that lives inside a query source, so it is only
// consulted once the turn is already IntentQuery. A phrasing the model calls
// system or note never reaches it, and "пока не умею" is what he hears — the
// failure the agenda and feed rules were written for.
//
// Placed after the agenda rules, which already send "какие у меня задачи" to
// query. What this adds is the phrasings with no possessive in them.
func TaskListGrammar() Grammar {
return Grammar{
Name: "task-list-query",
Pattern: regexp.MustCompile(`(?s)^\s*(.+)$`),
Build: func(m []string) (Decision, bool) {
if !IsTaskListQuery(m[1]) {
return Decision{}, false
}
return Decision{
Stage: 0,
Intent: IntentQuery,
Confidence: 1.0,
Slots: Slots{Text: strings.TrimSpace(m[1])},
}, true
},
}
}
+24
View File
@@ -119,3 +119,27 @@ func TestTaskCaptureGrammarClaimsTheMarker(t *testing.T) {
}
}
}
// TestTaskListGrammarClaimsTheAsk — a list question answered before the model,
// including the phrasings with no possessive that used to route elsewhere.
func TestTaskListGrammarClaimsTheAsk(t *testing.T) {
g := TaskListGrammar()
claimed := []string{"какие у меня задачи", "список дел", "что мне нужно сделать"}
for _, u := range claimed {
m := g.Pattern.FindStringSubmatch(u)
if m == nil {
t.Fatalf("%q did not match the grammar pattern", u)
}
d, ok := g.Build(m)
if !ok || d.Intent != IntentQuery {
t.Errorf("%q built %+v ok=%v; want a query", u, d, ok)
}
}
passed := []string{"как дела", "напомни купить хлеб", "что docker делает"}
for _, u := range passed {
m := g.Pattern.FindStringSubmatch(u)
if _, ok := g.Build(m); ok {
t.Errorf("%q was claimed as a task list", u)
}
}
}