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
3.9 KiB
Russian acts reach tools
06-08-2026. V-633. Measured with TestONNXBaseline, 91-case RU routing fixture,
classifier plus the ONNX embedder. No LLM arm in this run.
What was wrong
Three defects, tangled enough that fixing one alone would have looked like progress.
No Russian utterance could reach a tool. DefaultActMatcher in
internal/router/slots.go matched an exact English prefix, and internal/tool.Matcher
delegated straight to it. Its comment claimed "the production matcher is fuzzy, this is the
scaffold floor". There is no other matcher, and DefaultGrammars is the only place
Slots.Fn is set at stage 0, so the floor was the ceiling. Measured with a throwaway
matcher test over the seeds:
"покажи статус nginx" ok=false "restart nginx" ok=true fn=restart
"сколько места на диске" ok=false "disk" ok=true fn=disk
"свободная память" ok=false "uptime" ok=true fn=uptime
"перезагрузи роутер" ok=false
55 of the 69 lines in models/seeds/act.txt routed to IntentAct and then fell to
proposeGap. Praxis was never affected: PraxisGrammars fills Slots.Fn itself.
Seven lines were duplicated inside models/seeds/query.txt. A duplicate is a second
identical vector, so it double-weights its region in nearest-neighbour scoring.
сколько человек дома
кто сейчас дома
какая загрузка процессора
сколько свободного места на диске
какой ip адрес у сервера
какая версия софта
сколько оперативной памяти свободно
как дела у сервера carried two labels, in query.txt:13 and system.txt:9. One
string, two identical vectors, disagreeing about the answer.
The change
Tools carry spoken aliases as config data, in deploy/mavend.json. They are not a Russian
stem pattern in code, which CLAUDE.md forbids. They are not on the tool row either. An
ad-hoc tool enabled through /tools has no aliases and needs none.
Aliases and names compete in one table, longest phrase first, so "перезагрузи контейнер"
beats "перезагрузи" and "docker-restart" is not shadowed by "restart". Matching is on exact
leading tokens rather than lemmas. перезагрузи роутер is a command and перезагрузил роутер is a fact, and a lemma cannot tell the two apart. That is the trap
cmd/mavend/quiet_toggle.go documents for говори.
The seven duplicates are gone, and как дела у сервера stays in query.txt only. It left
system.txt because system cannot answer it: replySystem's
память/загрузк/аптайм arm returns "системная статистика пока не подключена." and always
did. That arm is a stub, not a mode, so the mode inventory now lists the shape as
act.tool.hoststats.
Result
69/91, 75.8% full, unchanged. Clarify counts unchanged at 0 false and 8 missed.
Nothing moved, and that is the honest number. The fixture holds no host-stat case and no
Russian act that reaches a tool, so it cannot see either fix. The new coverage is
TestActMatcherAliases, which asserts the twelve utterances above plus the two refusals.
What this does not fix
Argument quality. статус sshd reaches systemctl status sshd, but логи nginx reaches
journalctl -n 50 -u nginx only because the tool's argv prefix ends in -u. An alias whose
remainder is a Russian noun ("перезагрузи роутер") hands systemctl restart роутер a target
that does not exist. Free text still reaches an argv, which is the resolution rule the
ecosystem contract states for Hexis and not yet true here.
The fixture cannot measure any of this. That is the observability gap V-629 is for.