a Russian act reaches a tool, and the seeds stop disagreeing (V-633)

Three tangled defects, fixed together because each one hid the others.

DefaultActMatcher matched an exact English prefix and internal/tool.Matcher
delegated straight to it, so no Russian utterance could reach a tool: 55 of the
69 lines in models/seeds/act.txt routed to IntentAct and fell to proposeGap.
Tools now carry spoken aliases from deploy/mavend.json, matched as exact leading
tokens, longest phrase first. Config data, not a stem pattern in code. The
comment claiming "the production matcher is fuzzy" was false and is gone.

Seven lines were exact duplicates inside models/seeds/query.txt, each one a
second identical vector double-weighting its region.

"как дела у сервера" carried both a query and a system label. It leaves
system.txt, because replySystem's stats arm answers "системная статистика пока
не подключена." and always did. The mode inventory records that shape as
act.tool.hoststats rather than a system mode.

Fixture unchanged at 69/91, and it cannot see any of this: no host-stat case and
no Russian act in it. TestActMatcherAliases is the coverage.
docs/evals/2026-08-06-russian-acts-reach-tools.md has the numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117tgnmbgZpHVV3XSNw8Qua
This commit is contained in:
2026-08-06 18:09:11 +04:00
parent 1f8e9f21ce
commit e7ecce2859
9 changed files with 254 additions and 38 deletions
+16 -3
View File
@@ -222,11 +222,23 @@ func runProcess(ctx context.Context, argv []string) (string, error) {
// router's default prefix logic over the current names. The interface's Match
// has no ctx, so it queries with a background context — an in-process sqlite
// read on the daemon.
type Matcher struct{ api API }
// Aliases are spoken phrases per tool name, wired from the deployment config so
// a Russian utterance can reach an English tool name. They are not stored on the
// tool row: an ad-hoc tool enabled through /tools has no aliases and needs none.
type Matcher struct {
api API
aliases map[string][]string
}
// NewMatcher builds a store-backed act matcher.
func NewMatcher(api API) *Matcher { return &Matcher{api: api} }
// WithAliases returns the matcher carrying spoken aliases per tool name.
func (m *Matcher) WithAliases(a map[string][]string) *Matcher {
m.aliases = a
return m
}
func (m *Matcher) names() []string {
ts, err := m.api.ListTools(context.Background(), "enabled")
if err != nil {
@@ -243,7 +255,8 @@ func (m *Matcher) names() []string {
// Allowlist — the enabled verbs (for stage-0 grammar wiring / introspection).
func (m *Matcher) Allowlist() []string { return m.names() }
// Match — longest-verb-first prefix match over the live enabled allowlist.
// Match — longest-phrase-first prefix match over the live enabled allowlist and
// its configured aliases.
func (m *Matcher) Match(utterance string) (string, []string, bool) {
return router.DefaultActMatcher{Fns: m.names()}.Match(utterance)
return router.DefaultActMatcher{Fns: m.names(), Aliases: m.aliases}.Match(utterance)
}