Compare commits

..

15 Commits

Author SHA1 Message Date
claude 8d46ee39e0 Merge pull request 'Bug: the router transliterates Latin entity names into Cyrillic before Nexus sees them' (#131) from task/476-bug-the-router-transliterates-latin-enti into master 2026-08-04 18:23:16 +02:00
claude 6eba79b332 Merge pull request 'Decide whether a parked clarify question should survive a restart' (#130) from task/385-decide-whether-a-parked-clarify-question into master 2026-08-04 18:23:12 +02:00
claude 3f60ec3994 Merge pull request 'Backfill routines accepted before the fire-forever fix' (#129) from task/377-backfill-routines into master 2026-08-04 18:23:08 +02:00
claude 14ea06712e Merge pull request 'Weather: the 6-city match table has no geocoder behind it' (#128) from task/421-weather-geocoder into master 2026-08-04 18:23:03 +02:00
claude 42d3feadd2 Merge pull request 'No read path for delivery_attempts — the outbox is durable but invisible' (#127) from task/390-no-read-path-for-delivery-attempts into master 2026-08-04 18:22:58 +02:00
claude 2a2f706b74 Merge pull request 'Recall fixture: filler note ids can collide with case ids and split the two backends' (#126) from task/386-recall-fixture-filler-note-ids into master 2026-08-04 18:22:52 +02:00
claude e082e06868 Merge pull request 'Bug: morning.Item has no required/optional flag, so behaviour 1 of task 280 cannot hold' (#125) from task/473-bug-morning-item-has-no-required-flag into master 2026-08-04 18:22:45 +02:00
claude 88dc4e1383 Merge pull request 'Bug: make simulate routes with an empty seed set, so a green run proves less than it looks' (#124) from task/465-bug-make-simulate-routes-with-an-empty into master 2026-08-04 18:22:37 +02:00
claude 60759a991e Merge pull request 'Bug: spoken task capture is dead — the router calls the marker an act, and capture only rides the note intent' (#123) from task/467-bug-spoken-task-capture-is-dead into master 2026-08-04 18:22:28 +02:00
claude 9537346441 Merge pull request 'Bug: a pending clarify is global, so one unanswerable question swallows the next three utterances from anybody' (#122) from task/466-bug-a-pending-clarify-is-global-so-one-u into master 2026-08-04 18:21:49 +02:00
claude c6be818f13 Merge pull request 'Bug: pattern.Detect has no minimum-interval floor, so four fast taps mint a permanent false routine' (#121) from task/468-bug-pattern-detect-has-no-minimum-interv into master 2026-08-04 18:21:38 +02:00
claude b5575a9402 Merge pull request 'Bug: CheckFeminine flags second-person masculine verbs as self-reference' (#120) from task/462-bug-checkfeminine-flags-second-person-ma into master 2026-08-04 18:21:27 +02:00
claude fcda5e3d2c Merge pull request 'safeKey drops Cyrillic, so Russian calendar events on one day collide' (#119) from task/443-safekey-drops-cyrillic-so-russian-calend into master 2026-08-04 18:21:15 +02:00
claude bdafc82e35 Merge pull request 'llama-server core-dumps on every SIGTERM, so each mavgpud yield writes a core file' (#116) from task/491-llama-server-core-dumps-on-every-sigterm into master 2026-08-04 14:09:25 +02:00
claude c69023c310 Merge PR #117 into task/491 (V-383) 2026-08-04 14:08:59 +02:00
8 changed files with 0 additions and 327 deletions
-3
View File
@@ -57,9 +57,6 @@ var actionHandlers = map[router.Intent]func(*reactiveHandler, context.Context, r
func (h *reactiveHandler) actionChat(ctx context.Context, dec router.Decision) string {
// Conversational: build history from dialogue session (prior user turns)
// and let the LLM respond from general knowledge + context.
if h.phraser == nil {
return "поговорили."
}
history := h.chatHistory()
reply, err := h.phraser.PhraseChat(ctx, dec.Utterance, history)
if err != nil {
-15
View File
@@ -38,21 +38,6 @@ func (h *reactiveHandler) actionFact(ctx context.Context, dec router.Decision) s
q.Slots.Value = ""
return h.actionQuery(ctx, q)
}
// A complaint is not a fact either (#481). "сеть какая-то медленная" and
// "интернет не работает" were stored as `self` rows at confidence 1.00, and
// recall reads a self row back later as if it were still true — the same
// class of row that outranked live search in #470. The sentence describes a
// moment, so she answers it and stores nothing. An explicit "запомни ..."
// and anything about him are both left alone by the test.
if router.IsTransientComplaint(dec.Utterance) {
log.Printf("voice: fact write refused, utterance is a passing complaint: %q (key %q) — answering as chat",
dec.Utterance, dec.Slots.Key)
c := dec
c.Intent = router.IntentChat
c.Slots.Key, c.Slots.HasKey = "", false
c.Slots.Value = ""
return h.actionChat(ctx, c)
}
now := h.now()
req := ipc.WriteFactReq{
Ts: now,
-5
View File
@@ -81,11 +81,6 @@ var querySources = []querySource{
// matcher requires a task noun or an explicit "что … сделать", so a
// date-bearing question still reaches the calendar.
{name: "tasks", answer: (*reactiveHandler).queryTasks},
// Next to "tasks" and for the same reason: "что требует внимания?" is a
// question about the operational state Praxis holds, and it used to fall
// through every source to the web search (Vikunja #475). Its matcher needs
// an attention marker, and it falls through when Praxis is not configured.
{name: "attention", answer: (*reactiveHandler).queryAttention},
// Before the recall sources too: "сколько я потратил?" is a question about
// the money facts the poller wrote, and the notes pass would otherwise
// answer it from whatever he once said about spending. Its matcher needs a
-68
View File
@@ -1,68 +0,0 @@
package main
import (
"context"
"strings"
"github.com/kami/maven/internal/router"
)
// attentionMarkers — the ways he asks what Praxis is holding. Substrings on a
// stem, because "внимание", "внимания" and "вниманию" are one word to him.
//
// "что нового" is deliberately absent: the feeds source claims it, and it
// still should — a question about news is a question about the feeds she
// reads. This list is about the operational state of his things.
var attentionMarkers = []string{
"внимани", "что требует", "что не так", "что важн", "что срочн",
"needs attention", "what needs looking",
}
// isAttentionQuery reports whether the utterance asks what needs looking at.
func isAttentionQuery(u string) bool {
s := strings.ToLower(strings.TrimSpace(u))
if s == "" {
return false
}
for _, m := range attentionMarkers {
if strings.Contains(s, m) {
return true
}
}
return false
}
// queryAttention answers "что требует внимания?" from Praxis.
//
// The capability was already built and already degraded correctly, and no
// utterance could reach it (Vikunja #475). Its aliases live on the act
// dispatch, and the question routes to IntentQuery, so it fell through every
// source to the web search and came back with an encyclopedia article about
// the concept of attention — worse than silence, because it reads as an
// answer.
//
// Placed above the recall sources and well above the personal boundary: this
// is operational state about his things, and a notes pass would otherwise
// answer it from whatever he once wrote about a server. An unconfigured or
// absent Praxis falls through rather than claiming the turn, the same
// convention queryHome and queryNetwork follow. A Praxis that is configured
// and down does claim it, and says it cannot reach the service — that is the
// degradation the ecosystem contract asks for, and it comes from the same
// handler the act path uses.
func (h *reactiveHandler) queryAttention(ctx context.Context, t *queryTurn) (string, bool) {
if !isAttentionQuery(t.dec.Utterance) {
return "", false
}
if h.ecosystem == nil || h.ecosystem.praxis == nil {
return "", false
}
reply := h.handlePraxisAct(ctx, router.Decision{
Utterance: t.dec.Utterance,
Intent: router.IntentAct,
Slots: router.Slots{Fn: "list_attention", HasFn: true},
})
if reply == "" {
return "", false
}
return reply, true
}
-77
View File
@@ -1,77 +0,0 @@
package main
import (
"context"
"strings"
"testing"
"time"
"github.com/kami/maven/internal/router"
)
func TestIsAttentionQuery(t *testing.T) {
for _, tc := range []struct {
text string
want bool
}{
{"что требует внимания", true},
{"на что обратить внимание?", true},
{"что не так?", true},
{"что важного?", true},
// The feeds source owns this one, and should keep owning it.
{"что нового?", false},
{"какая погода?", false},
{"", false},
} {
if got := isAttentionQuery(tc.text); got != tc.want {
t.Errorf("isAttentionQuery(%q) = %v, want %v", tc.text, got, tc.want)
}
}
}
// TestAttentionQuestionReachesPraxis — the defect (Vikunja #475). The question
// routes to IntentQuery, and every source used to pass, so a web search about
// the concept of attention answered it.
func TestAttentionQuestionReachesPraxis(t *testing.T) {
ctx := context.Background()
praxis := newFakePraxis(t, fixturePraxisAttentionItems(map[string]any{
"id": "item_1", "title": "disk almost full", "importance": 3.0,
}))
h := ecoHandler(t, nil, praxis, nil)
reply, ok := h.queryAttention(ctx, &queryTurn{dec: router.Decision{
Intent: router.IntentQuery, Utterance: "что требует внимания",
}})
if !ok {
t.Fatal("the attention question must be claimed before the world sources")
}
if !strings.Contains(reply, "disk almost full") {
t.Fatalf("reply = %q, want the praxis item", reply)
}
}
// A configured Praxis that is down claims the turn and says so. Falling
// through here would answer an outage with an encyclopedia article.
func TestAttentionQuestionSaysWhenPraxisIsDown(t *testing.T) {
ctx := context.Background()
praxis := newFakePraxis(t, fixturePraxisAttentionItems())
h := ecoHandler(t, nil, praxis, nil)
praxis.SetFault(503)
reply, ok := h.queryAttention(ctx, &queryTurn{dec: router.Decision{
Intent: router.IntentQuery, Utterance: "что требует внимания",
}})
if !ok || !strings.Contains(reply, "не могу") {
t.Fatalf("an outage must name the gap, got ok=%v reply=%q", ok, reply)
}
}
// No Praxis configured means no claim: the rest of the chain still runs.
func TestAttentionQuestionFallsThroughWithoutPraxis(t *testing.T) {
h, _ := newFactGateHandler(t, time.Now())
if _, ok := h.queryAttention(context.Background(), &queryTurn{dec: router.Decision{
Intent: router.IntentQuery, Utterance: "что требует внимания",
}}); ok {
t.Fatal("an unconfigured praxis must not claim the turn")
}
}
-45
View File
@@ -123,48 +123,3 @@ func mustEmbedPassage(t *testing.T, h *reactiveHandler, text string) []float32 {
}
return vec
}
// The write half of #481: a complaint about a thing is a state of the
// afternoon, not a fact about him. Stored as a `self` row at confidence 1.00
// it comes back on recall as if the network were still down.
func TestActionFact_ComplaintIsNotWritten(t *testing.T) {
ctx := context.Background()
h, api := newFactGateHandler(t, time.Now())
reply := h.actionFact(ctx, router.Decision{
Intent: router.IntentFact,
Utterance: "сеть какая-то медленная",
Slots: router.Slots{Key: "network_speed", HasKey: true, Value: "медленная"},
})
if _, err := api.LatestFact(ctx, "network_speed"); err == nil {
t.Fatal("a passing complaint was stored as a fact about him")
}
hits, err := h.memStore.Search(ctx, mustEmbedPassage(t, h, "сеть какая-то медленная"), 3)
if err != nil {
t.Fatalf("memory search: %v", err)
}
if len(hits) != 0 {
t.Fatalf("the complaint was indexed for recall: %+v", hits)
}
if reply == "" {
t.Fatal("the turn was neither stored nor answered")
}
}
// And the complaint he asked her to keep: the capture verb wins, as it does
// over the question gate.
func TestActionFact_AskedToRememberAComplaintStillWrites(t *testing.T) {
ctx := context.Background()
h, api := newFactGateHandler(t, time.Now())
h.actionFact(ctx, router.Decision{
Intent: router.IntentFact,
Utterance: "запомни что интернет не работает",
Slots: router.Slots{Key: "internet", HasKey: true, Value: "не работает"},
})
if _, err := api.LatestFact(ctx, "internet"); err != nil {
t.Fatalf("an explicit capture was refused: %v", err)
}
}
-79
View File
@@ -1,79 +0,0 @@
package router
import "strings"
// transientStems — the states a thing is in for an afternoon. Compared as
// prefixes because Russian inflects the ending: "медленн" covers "медленная",
// "медленный" and "медленно" without listing them.
var transientStems = []string{
"медленн", "тормоз", "лаг", "завис", "виснет", "глюч", "барахл",
"отвал", "падает", "упал", "сдох", "греется", "перегре",
"slow", "laggy", "stuck", "frozen", "flaky", "broken", "down",
}
// brokenVerbs — what "не ..." is denying when the sentence is a complaint.
// "не работает", "не грузит", "не открывается". Prefixes again.
var brokenVerbs = []string{
"работ", "пашет", "груз", "открыва", "включа", "коннект", "подключ",
"work", "load", "connect", "respond",
}
// selfMarkers — the words that make a sentence about him rather than about a
// thing. Their presence turns the test off, because losing a fact he meant to
// store is worse than keeping a complaint: "я сломал руку" is durable, and
// "интернет не работает" is not.
var selfMarkers = []string{"я", "мне", "меня", "мной", "i", "me", "my"}
// IsTransientComplaint reports whether text observes a passing state of some
// thing rather than recording a fact.
//
// It exists because "сеть какая-то медленная" and "интернет не работает" were
// written to the fact store as `self` rows at confidence 1.00 (Vikunja #481),
// where recall reads them back later as if they were still true. A complaint
// describes a moment; the fact store describes him.
//
// Deterministic, offline, and shaped exactly like IsQuestionShaped: an
// explicit capture verb wins over everything, because "запомни что интернет
// не работает" is an instruction and not a passing remark. A first-person
// marker also turns it off — the test is meant to catch a sentence about a
// thing, and it errs toward storing.
func IsTransientComplaint(text string) bool {
t := strings.TrimSpace(text)
if t == "" {
return false
}
toks := planTokens(strings.ToLower(t))
for _, v := range captureVerbs {
if hasTok(toks, v) {
return false
}
}
for _, m := range selfMarkers {
if hasTok(toks, m) {
return false
}
}
for _, tok := range toks {
for _, stem := range transientStems {
if strings.HasPrefix(tok, stem) {
return true
}
}
}
// "не" plus a verb of working, in either order of the two tokens that
// follow it — "не работает" and "не очень работает" both deny the same
// thing.
for i, tok := range toks {
if tok != "не" && tok != "not" && tok != "isn" {
continue
}
for j := i + 1; j < len(toks) && j <= i+2; j++ {
for _, v := range brokenVerbs {
if strings.HasPrefix(toks[j], v) {
return true
}
}
}
}
return false
}
-35
View File
@@ -1,35 +0,0 @@
package router
import "testing"
func TestIsTransientComplaint(t *testing.T) {
for _, tc := range []struct {
text string
want bool
}{
// The two rows from the QA run that named this bug.
{"сеть какая-то медленная", true},
{"интернет не работает", true},
{"вайфай тормозит", true},
{"сервер завис", true},
{"the wifi is slow", true},
// An instruction wins: he asked for it to be written down.
{"запомни что интернет не работает", false},
{"запиши что сеть медленная", false},
// About him, so it stays a fact even when it sounds like a complaint.
{"я сломал руку", false},
{"мне медленно думается", false},
// Ordinary captures must not be touched.
{"поужинал", false},
{"выпил воды", false},
{"машина на парковке", false},
{"", false},
} {
if got := IsTransientComplaint(tc.text); got != tc.want {
t.Errorf("IsTransientComplaint(%q) = %v, want %v", tc.text, got, tc.want)
}
}
}