Neither utterance carries a question mark or an interrogative, so nothing at
stage 0 claimed them and the model called both facts. The write is contained —
actions_fact refuses a question-shaped fact and re-runs the turn as a query —
but every one of these paid a full model round trip to reach a decision two
regexes can make, and the fixture scored the routing as wrong.
rest-of-day-query joins the agenda grammars: the predicate for the utterance
already existed as IsRestOfDayQuery, one layer down in the query chain, and
this is what gets the turn there. NarrativeQueryGrammar reads the same
narrativeRequests lexicon IsQuestionShaped reads, and declines the topics that
are chat rather than world questions — a joke, a bedtime story, herself. It is
wired last, so an explicit capture marker still wins.
Fixture: ru-query-024 and ru-query-025, both passing. Classifier + ONNX
baseline 56/80 (70.0%) → 58/82 (70.7%), no case regressed and no new false
clarify. The LLM arm is unmeasured here — no llama-server in this run.
The mavweb auth test posted its instant as "Z", which the #482 fix now reads in
the daemon's zone, making the clock inside the text stale by the test box's own
offset. It carries the local offset now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 60s wait for llama-server's listen line was hardcoded, so the last arm of
the startup race could not be tested without waiting a real minute, and a box
where a cold 1.7B loads off spinning disk had no way to raise it.
Config.StartupTimeout, defaulted to 60s. The test drives the arm at 200ms
against a fake server that never listens, and asserts the child is killed and
reaped — that arm leaks a llama-server still loading a model otherwise.
startLlamaProc 90.9% → 96.0%, package 76.9% → 77.6%.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A relay that posts its instant as `2026-08-02T09:00:00Z` handed the wall clock
inside the text that same zone, so «созвон в 14:30» was built as 14:30 UTC and
read back as 18:30 on this UTC+4 box. Every ambient event landed late by the
deploy's own offset, and correct on a UTC box, which is why no test caught it.
Posted is an instant and carries a zone. The clock reading is a wall clock and
carries none, so it resolves against the daemon's zone now. The tests pin
time.Local to +04 in TestMain, so the four hours show up on a UTC runner too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The checks stay in the eval package and the daemon calls three of them:
feminine, address, and a new leaked-reasoning test. No retry — it doubles
the latency on the turn that is already going badly, and on the nudge path
the moment has passed. A failure falls back to the deterministic floor and
is logged with the whole rejected text and counted by check name.
hisgender is deliberately not run: the simulator showed it rejecting
"записала, что ты выпил воды", which is her own correct self-reference.
The capture half landed with the grammar in 87d1761. This is the exposure
the task asked to check for: IsTaskListQuery is a deterministic lookup that
only runs once the turn is already a query, so a phrasing the model calls
system never reaches it. The eval fixture was also missing both grammars,
which is only worth having while it is the daemon's grammar set.
Read before routing and only when a list is bound: with nothing offered,
"второй" is an ordinary word and keeps routing. No verb reads it back
rather than guessing what to do with it.
Session.Candidates holds what she offered, in the order she offered it, and
SetCandidates attaches it in place so the turn already remembered keeps its
slots. tasks.Spoken is the list FormatRU actually named, so an ordinal and
the spoken order cannot drift apart.
Read next to the confirm and clarify turns, because a correction routed as
a fresh utterance files the correction itself. Only turns she acted on are
remembered: a clarify asked instead of acting.
CorrectMisroute has been in the router since it was written with no caller
outside a test. repair.go is the half that reads the words: a marker saying
she was wrong plus the intent it should have been, with the negated half
skipped, and it teaches the classifier and redoes the request under the
corrected intent.
The vendored toolchain lives inside the module tree, so `go mod tidy` walked
Go's own compiler-error fixtures and died on files that are malformed on
purpose ("unicode//utf8": double slash). A nested module is not part of its
parent, so deps/go.mod ends the walk in three lines. deps/ is gitignored, so
the sentinel is generated by `make deps-sentinel`, which deps-go and deps now
depend on.
The tidy it makes possible drops github.com/kami/praxis, which no file
imports — Praxis is reached over HTTP, by contract. Its replace directive and
the unused nexus one went with it, so a build no longer expects two sibling
checkouts that nothing reads. vendor/ is committed, so `make tidy` re-vendors
in the same breath: a tidy alone leaves the next build failing on
"inconsistent vendoring".
Not wired into `make test`. A build target that rewrites go.mod is a surprise.
Verified: `make build` produces all 9 binaries and `make test` is green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
piper reads a Russian sentence with a Russian voice, and a Latin service id
inside it comes out spelled, mangled or read as if it were a Russian word:
"Vikunja", "SearXNG", "homesrv". The lever available is the text, so the
dictionary maps a name to how it should be spelled for the voice to say it,
and mavttsd applies it at the last edge before piper — every caller's text
passes through that one point, and nothing upstream has to know how a name
sounds.
Data, not code. deploy/tts-lexicon.json ships 29 names; adding one needs a
restart of mavttsd and no rebuild of the daemon that produced the text. Off
unless -lexicon is set, like every other optional capability, and a path that
is set and unreadable stops startup — saying names wrong in silence is the
failure it exists to remove.
Two details worth keeping: the alternation is sorted longest-first, or "Home
Assistant" reads as "Хоум Assistant"; and the boundaries are written out
rather than left to \b, which is ASCII-only and never fires next to a
Cyrillic letter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read-only over rows that exist. No new mechanism and no new storage: every
fact he tapped in already carries a source and a timestamp, and the history
source only reads them back.
Only "tap:" sources, and only the last day. A fact written by a poller, an
inference or the ambient relay is a thing she learned rather than a thing he
said, and reading those back under "что я тебе говорил?" would put words in
his mouth. Five at a time, which is what fits in one spoken breath — the rest
are on /history, which is the surface for reading a list.
Above the recall sources, with the others that read his own rows: the notes
pass would otherwise answer this from whatever note is nearest, which reads
as an answer and is not one. The matcher wants both halves of a history
phrase and steps aside when he names a topic, so "что я говорил про сервер"
stays a recall question.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every clarify turn said one sentence per gap, and a re-ask repeated it word
for word. A question he already failed to answer is the worst one to ask
again unchanged: the second wording is what tells him which part she missed.
clarifytemplates.go holds three wordings per slot, picked by attempt rather
than at random — short first, then naming the gap, then spelling it out with
an example. Past the end she keeps the most explicit one instead of wrapping
back to the short question he has already not answered.
The intents with nothing identifiable to ask about (note, query, chat,
system) kept the stub's single "не совсем поняла — можешь переформулировать?",
which is the line he hears whenever she misses him completely. Four wordings
now, picked by a hash of the utterance so one question asked twice reads the
same and two different misses do not.
Still no model call on this path: the resident model would wander, and this
text has to be right every time. No schema_version either, unlike the nudge
templates — these are Go constants, so no file can drift out of step with the
code that reads it. The persona test already in clarify_test.go covers the
new lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The querySources order predates the 2026-08-02 ruling that live search leads.
An unconfigured feeds source claimed every news question and answered with a
configuration status, so "что происходит сейчас в новостях про искусственный
интеллект?" never reached the search sitting one source below. It now claims
only when neither SearXNG nor the ZIMs are configured, which is the case the
"не читаю ленты" line was written for — general knowledge would otherwise
invent a bulletin.
The calendar matches on a day word alone and sits above the weather, so
"какая сегодня погода в Москве?" answered "на 02.08.2026 ничего нет." It now
steps aside on weather wording, the same bail-out queryHome already does.
"что нового в лентах?" routed system and answered "пока не умею", while the
same question worded with "новостях" worked. FeedQueryGrammar routes it to
query at stage 0, requiring an ask word and a feed noun so the bare greeting
"что нового?" stays a greeting. Wired in the eval too, since the fixture is
only worth anything while its grammar set is the daemon's.
Also: the claiming source is now logged. /trace is the nudge-rule trace and
carries no query-source field, so a wrong answer could not be told apart from
a wrongly-ordered chain.
Kiwix having no live coverage is filed separately as V-508 — it is a decision
about search quality, not an ordering fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two of the four defects on the task.
The stored payload was the whole utterance, so /reminders and the agenda
recited "напомни завтра в 9 утра выпить таблетки" where the reminder is
"выпить таблетки". The marker is an instruction that was already carried out
and the hour is already a column, so reminderBody strips both, and falls back
to the unstripped body whenever stripping would leave nothing — a reminder
that fires and says nothing is worse than a wordy one.
The page rendered the raw {"text":...} envelope and the UTC instant. Both are
now done in mavweb: reminderRows unwraps the payload and formats through
Local(). The unwrap is a copy of store.ReminderText rather than a call to it,
because mavweb builds without CGO and internal/store carries the sqlite
driver — the ipc DTOs are decoupled from the store on purpose.
TestClarifySubjectAnswerFillsRatherThanClobbers asserted the hour survived as
a word in the payload. It now asserts the fire time, which is where the hour
lives.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"напомни мне позвонить маме в семь вечера" answered "не получилось разобрать
время напоминания", while "в 19:00" set the reminder. Reminders arrive through
speech, and speech says the hour in words, so this was the ordinary case
failing and the typed one working.
SpellOutDigits rewrites a spoken number as digits, but only when a time word
stands beside it — "в три часа" becomes "в 3 часа" and "купить три яблока" is
left alone. Both parsers see it: dateparser already rewrites "7 вечера" to
"7 pm" and never saw a digit to rewrite, and the stub floor now reads the
qualifier itself.
The capability was built, wired and degrading correctly, and no utterance
could reach it. Its aliases sit on the act dispatch, "что требует внимания"
routes to a query, and every query source passed — so the turn fell to the
web search and came back with an article about the concept of attention.
That reads as an answer, which is worse than silence.
queryAttention sits next to "tasks", above the recall sources and well above
the personal boundary: it is operational state about his things, and a notes
pass would otherwise answer from whatever he once wrote about a server. It
calls the same handler the act path calls, so the outage string comes free.
An absent or unconfigured Praxis falls through instead of claiming the turn,
like queryHome and queryNetwork. A configured Praxis that is down claims it
and names the gap. "что нового" is left to the feeds source.
"сеть какая-то медленная" and "интернет не работает" were written as `self`
rows at confidence 1.00. Recall reads a self row back later as if it were
still true, and that is the class of row that outranked live search in #470 —
so a slow afternoon becomes a standing belief about his network.
IsTransientComplaint is the same shape as IsQuestionShaped: deterministic,
offline, and off by default in the two cases where losing a real capture would
cost more than keeping a complaint. An explicit "запомни ..." wins, because he
asked. A first-person marker wins, because "я сломал руку" is durable and the
test is meant for sentences about things.
She answers the turn as chat instead of storing it. actionChat now has the
same nil-phraser floor the other model callers have.
The second defect filed here — a reply body of literally "{" — was closed by
the errBrokenJSON path in V-397 and needs nothing further.
Two defects in one logged line, both of which put a working capability out
of reach of every utterance.
The resident model rewrites as it routes, and on the way it transliterates:
"перезапусти muzick indexer" came back as "перезагрузить музик индексер", so
Nexus was asked to resolve a service nobody has ever named. entityReferenceText
takes the longest Latin run out of his own words, but only when the Text slot
has lost every Latin letter the utterance had — an English turn and a Russian
entity name are both left alone, and reversing the transliteration is not
attempted.
The second half: the stage-3 gate thins an act that matched no allowlisted fn,
and that question was the whole turn, so handleHexisAct never ran. Hexis is
where an act with no local fn belongs, so it gets one chance before she asks,
and a "" back still leaves her asking. With no ecosystem wired nothing changes.
Capability matching reads the phrase as the haystack when there is no fn,
because no capability name contains "restart status muzick indexer".
Authority is untouched: ambiguity still stops, a mutating capability still
goes through the spoken confirm.
The decision, not a behaviour change: ClarifyStore stays in memory, and she
does not announce the loss either.
The TTL and the attempt count measure a pause in one conversation. A restart
is a gap of unknown length, so a restored question is either dead already or
lying about its age, and the request behind it is one he has likely given up
on. Announcing it would mean storing a marker that outlives the thing it
describes, to say one sentence in the rare window where he speaks within 90s
of a restart. His next words route fresh, which is right either way.
Written down in docs/design.md, pinned at both ends by a comment, and held by
a test that builds a second handler over the same store.
Routines accepted before Vikunja #366 carry accepted_ts NULL and a live
reminder row. The tick loop reads accepted_ts to decide when a routine is
next due, so those rows have been silent since the fix landed, while the
reminder they still point at keeps firing on its own schedule.
Migration #19 cancels that reminder first, then dates the acceptance from
created_ts and lets the reminder id go. Order matters: the second update
clears the id the first one needs.
The hand-written table understood "какая погода в X" for six values of X.
Ask about Kazan or Tbilisi and the city was dropped silently and answered for
the default location — a correct-sounding answer about the wrong place.
The table is gone. internal/weather already calls Open-Meteo's geocoding
endpoint on every lookup, so the place he named goes straight there and any
place it knows is a place he can ask about. He speaks the prepositional case,
so locationCandidates reverses the two endings that cover most of it: a final
"е" is a nominative "а" or nothing, a final "и" is a soft sign. A wrong
candidate finds no city; it never invents one.
A place the geocoder does not have now reads as "не знаю такого города"
rather than as a provider outage or, worse, as the default city's weather.
ErrLocationUnknown is what carries that apart.
"в" followed by a room or a day word is still the default location. Those
questions are answered by the house sensors and the calendar, not by
Open-Meteo, and they must not be read as a city.
The table was write-only. Rows were recorded and nothing could show them, so
the tests for #368 and #370 had to reach past the store into store.DB — if a
test can only see it that way, so can nobody else. A durable record nobody
reads answers no question, and why Maven went quiet is supposed to be a query.
ListDeliveryAttempts returns recent rows newest first, filtered by status.
Status is the filter worth having because the two real questions are "what got
dropped" and "what is still pending", and neither is answerable by reading the
whole list on a busy day. It reaches mavweb over IPC as DeliveryAttempts.
The section goes on /notifications, which already answers "what did she send",
rather than on a page of its own. Shared ui.css, the nav partial, the table in
div.scroll. A failed outbox read leaves a log line and still renders the nudge
list, because half the page beats none of it.