Commit Graph

460 Commits

Author SHA1 Message Date
claude 8aba4845bf Merge remote-tracking branch 'origin/master' into task/486-deploy-the-workstation-transcriber 2026-08-09 01:22:03 +04:00
claude 2ec92ee8bf Merge pull request 'Move STT and TTS to the workstation, where the microphone already is' (#208) from task/486-move-stt-and-tts-to-the-workstation-wher into master 2026-08-08 23:21:51 +02:00
claude b975716759 One owner for the card, not two neighbours (V-486)
CW2 is a ROCm process, so it registers on the KFD like any contender. Running
it as its own systemd unit made mavgpud yield llama-server to it every few
seconds. The gemma-4-12b arm was down for eight minutes on 2026-08-09 and
routing had silently fallen back to the resident model.

So mavgpud takes an `stt` block and runs the transcriber itself. `foreign` now
excludes every child rather than one pid, which is the fix. Yielding is all or
nothing, because a job that wants the card wants all of it. Idle unloading
stays llama-server's alone: CW2 holds 1.6GB and unloading it would only send
the next voice turn to the homesrv floor.

Maven still talks to the transcriber directly on 8081. There is no proxy,
because with no idle timer there is nothing for one to measure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
2026-08-09 01:07:46 +04:00
claude cc32c2c4ab Wire the transcription seam beside the model seam (V-486)
sttSeam is modelSeam for audio and sits at the same place in wireVoice, so
the voice path and the meeting recorder share one transcriber as they
always have.

A box with no workstation.stt block behaves byte-for-byte as it did before
this existed: the floor is handed back untouched and nothing probes. An
empty URL is normalised to no block at all, the way the model block already
works.

Health defaults to the URL's origin rather than the URL itself, because the
transcribe endpoint names a path and appending would ask for
/transcribe/health. A block with no token logs once that anything on the
LAN can post audio to that port.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
2026-08-09 00:42:15 +04:00
claude a4abcdefa3 Give the daemon a heads_path and a fixture arm (V-664)
embedder.heads_path is empty by default and deploy/mavend.json sets
it. A missing or broken weights file logs and leaves the heads nil,
because refusing to start over a routing accelerator would trade a
working box for a better one.

TestONNXRoutingHeads is the same cascade TestONNXBaseline scores with
one arm added, so the two are directly comparable. It also checks the
Go tokenizer against the Python one, since the heads were trained
through transformers and are read through a hand-written tokenizer: a
mismatch shows up here as a score below what Python measured on the
same weights, and nowhere else. That is how the reversed word pieces
were found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
2026-08-08 22:24:47 +04:00
claude a886217223 A greeting is not a failed answer (V-663)
classifyTurnRole read "спасибо" and "привет" as answers to whatever was
parked, so she re-asked "В какой день?" at a man saying thank you and
spent one of three attempts doing it. That attempt is a bound meant to end
the ride, so the pleasantry both produced the worst reply in the corpus and
paid for the privilege.

They are asides now: answered as themselves, the question resumed on the
tail, no attempt spent, one ride counted.

The set is a new closed lexicon entry, matched as WHOLE utterances. Every
token rule tried was wrong on something. "вечер" answers "это утра или
вечера?" and "нет" answers a confirm, so anything that could fill a slot
stays out. The control words stay out too, because isCancel owns them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
2026-08-08 21:43:15 +04:00
claude de9884e063 Count the rides a question takes, without the reset (V-663)
MaxSuspends did not move the number it was written for. Twenty-six of 140
turns carried a parked clarify tail before it landed and twenty-six after.

Two bounds rearm each other. An aside spends no attempt, so MaxAttempts
never reaches it. A turn reading as a failed answer zeroes Suspends, so
MaxSuspends never reaches the asides. Alternating them restores each bound
with the other's traffic. Measured on 2026-08-08: one question about a
reminder's day rode turns 7 to 13.

PendingQuestion.Rides is the same event counted without the resets. Set
once, incremented only in noteSuspended, carried across the re-park in
askRemainingGap, read by nothing that could lower it. MaxRides is 4, one
looser than MaxSuspends so the tighter statement about a run stays
reachable.

It ends the measured ride one turn early and no more. Most of that ride is
attempts, spent because classifyTurnRole reads "спасибо" and "привет" as
failed answers. Said so in the constant and in the design doc rather than
claiming a fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
2026-08-08 21:38:32 +04:00
claude ab3e818bb9 A named destination silences the guessers and moves nobody (V-655)
querySources splits in two once you look at which sources over-claimed during
the week of 2026-08-07. The clean ones perform a lookup and can come back
empty: fact-by-key, tasks, list, money, calendar, notes. The dirty ones decide
by cosine against frozen seeds and then answer whatever they claimed, because
they have no lookup that could miss. Weather has no local table at all, which
is why "что такое TCP?" became "для какого города?".

So each source now carries its destination and whether it guesses, and
queryWalk takes the guessers that were not named OUT of the chain. It removes
and never reorders, which is the whole safety argument: the table's order is
load-bearing, every comment on it argues a reason between two sources, and
above all it carries "his data first, then the world". Naming SourceWorld does
not send the turn outside. It stops weather claiming a protocol on the way
past. His notes, his facts and the boundary in front of them still run first,
so a wrong destination costs nothing but the guess it prevented.

The skipped sources are recorded as never-asked with the reason, so /trace
shows a narrowed walk rather than a chain that silently shrank.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 13:02:13 +04:00
claude 4fdecf9a25 Let a question go after it has stepped aside three times (V-654)
A side query suspends the parked question rather than dropping it. Nothing bounded that. No attempt is spent, so MaxAttempts never applies, and noteSuspended restarts the 90s clock, so the TTL cannot arrive while he keeps talking. Measured 2026-08-07: one unfilled time slot rode the tail of six consecutive unrelated replies.

PendingQuestion.Suspends counts the step-asides, MaxSuspends is 3, and past it she lets the request go with the same clarifyDropped line every other drop uses. The count is of consecutive step-asides and resets the moment he answers.

Also splits the re-ask off the answer into its own sentence. The comma splice buried the question in the tail of a reply about something else.
2026-08-07 12:16:15 +04:00
claude beaa24754c Read the CalDAV password from a file, not from argv (V-644)
mavcaldav took -pass and -render-pass as flag values, so enabling it would
have put his calendar password in `ps` inside the container, in the compose
file, and in shell history. mavpoll and mavmaild both read their secret from
a file for exactly that reason.

readSecret reads once at start, trims, and refuses an empty or missing file.
An empty file is a deployment mistake, not a password, and basic auth would
otherwise send "" and collect a 401 every poll. A rotated password means a
restart, which is cheaper than re-reading the credential every five minutes.

Nothing called the old flags: no compose service, no systemd unit, no test.
So they are replaced rather than kept beside the new ones.
2026-08-07 01:19:33 +04:00
claude 2c0334c4fe Count the enrichment backlog without a second query (V-647)
`tick` read `PendingFactResolutions` at the scan limit, then `status`
read it again with the same limit for one log line. Up to 2000 rows per
tick on a database that serialises reads, to say how long the queue is.

`statusOf` counts over a batch the caller already holds, and the tick
passes it the batch it just read. A resolved fact leaves the queue, so
the loop collects what is still pending rather than reporting the
pre-tick count. `status(ctx)` stays as the querying form, for a caller
outside the tick with no batch in hand.

No behaviour change: the three counts still describe one row set, and
the same facts are attempted per tick.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:32:54 +04:00
claude b5ac48c126 One boot path for the workers and the API (#189) 2026-08-06 21:54:13 +02:00
claude 69d0f5ee78 No deadline survives the turn path, from mavweb down to llama-server (#188)
Co-authored-by: claude <no-reply@agents.claude.kvmx.ru>
Co-committed-by: claude <no-reply@agents.claude.kvmx.ru>
2026-08-06 21:11:42 +02:00
claude b3936348f5 gofmt the act target guard (V-634)
Landed unformatted, so make test failed on fmt-check for everyone after.
2026-08-06 20:55:53 +04:00
claude c61b0b3968 wiring the poller into both boot paths (V-637)
It reaches the daemon through ipc.CoreAPI and nothing else, so a telegram
turn takes the path POST /api/chat already takes: Chat returns the reply and
the trace id it collected off the context (V-630), and CorrectTurn writes
the label. Nothing in internal/delivery learns what a handler is.

Wired on the unlocked start and on the passkey unlock, like the mail intake,
so telegram behaves the same either way. A sink that will not build is
logged rather than fatal here, because wireDispatcher already failed the
boot on the same config.
2026-08-06 20:53:48 +04:00
claude d32eae8aac a spoken correction lands in the label table, with or without a target (V-636)
The gesture was web-only, so the sample was skewing to the turns he happens
to type. Voice is where the hard cases are.

Half of it already existed: the repair rung has read "нет, это была заметка"
since V-455. It taught the classifier and wrote no durable label, so the two
paths disagreed about what a correction is. It now writes both. Two sinks and
not one on purpose: the classifier seed makes the next turn better today, and
the label is what a fitted head trains on after the transcript expires.

The trace id is stamped onto the remembered turn after the fact, because the
trace is written when the turn ends and recordTurn runs in the middle of it.

New: the untargeted half. "нет, не так" writes the negative and redoes
nothing, because there is no target to redo it as. Voice needs this more than
the web does — naming an intent aloud means saying "заметка" or "факт",
which is her vocabulary and not his.

repair_negatives is a new closed lexicon set matched against the WHOLE
utterance, never as a substring. That is what keeps it apart from
repair_markers, where "это не" is a fragment that needs an intent word after
it. A member that could appear inside an ordinary sentence does not belong in
the set.
2026-08-06 20:12:19 +04:00
claude 0e82cb442f the unplaceable word rides a typed error, not the message (V-634)
Recovering it by cutting on quotes in err.Error() meant the reply depended on
the wording of an error string. UnknownTargetError carries the word and
errors.Is still holds.
2026-08-06 20:06:25 +04:00
claude d94ed2e630 an act with a target the system cannot have does not run (V-634)
V-633 gave tools spoken aliases, so a Russian act reaches a tool. It resolves
the verb only: the rest of the sentence became argv. "перезагрузи роутер" ran
as systemctl restart роутер, which is a real tool, a real word and a target
that cannot exist on this box. She then reported systemctl's own confusion as
if she had tried something sensible, and on a destructive row she spent a
confirm turn on it first.

The executor now refuses, ahead of the confirm gate, and names the word it
could not place. The check is the script and not a word list: a unit, a
container, a host and a path are ASCII here, so a Cyrillic argv element means
the alias match swallowed the verb and handed on the next word.

Process rows only. An MCP argument is not a target — a task title is Russian
and always was — and a house row drops the spoken args already.

It does not try to guess the right target. Identity is Nexus's, and a target
Nexus resolves reaches Hexis through handleHexisAct before this executor is
asked.
2026-08-06 20:05:34 +04:00
claude a4b4733767 the correction gesture is step-up gated after all (V-630)
Trace ids are sequential integers and the label table is the one thing the
routing heads will be fitted on, so an ungated POST let anyone past the
transport gate mislabel turns the owner never touched.

The cost argument for leaving it open does not hold: he tapped to send the
turn he is correcting, so the session is already up when the buttons appear.
2026-08-06 19:48:29 +04:00
claude 0d5bd0a9f0 one gesture beside the reply corrects a turn (V-630)
Two buttons' worth of cost: wrong, or wrong and it should have been this.
The second is worth much more and is not required to give the first, so a
turn marked wrong with no target still lands as a usable negative.

The target is one of the seven intents, never free text: an unroutable label
would enter the one table V-632 fits prototypes from.

/api/correct is not behind the step-up gate. It reaches no router, no model
and no act path, and a correction that costs a passkey tap is one that does
not get made.
2026-08-06 19:45:50 +04:00
claude 4d97280d74 a turn hands back its trace id, and one wire op corrects it (V-630)
The correction is the only supervised signal in the box, so the cost of
giving one has to be near zero. That means the surface needs the trace id of
the turn it is showing, which it had no way to learn: handleText returns one
string and the trace was written after the reply left.

The id rides back on ChatReply through the same context sink querySource
uses, so the mic, telegram and the web keep the one signature they share.
CorrectTurn takes a trace id and an optional target, which is deliberately
reach-agnostic: nothing about it assumes a browser.

store.ErrNoSuchTrace gets a wire twin. A turn past the retention bound is
gone, and that is the expected outcome of correcting an old turn, not a
broken database.
2026-08-06 19:45:37 +04:00
claude e1f84a3474 review: a cancelled turn keeps its trace, and a quiet box still expires (V-629)
Two defects found reviewing the PR.

The insert ran on the turn's own context, so a caller that hung up or timed out
cancelled it. That is exactly the turn worth having. It now runs detached, with
a one-second bound of its own, because a write must not hold the reply.

Retention was enforced on write alone, so a box that goes quiet for a month kept
every row until the next sixty-fourth turn. pruneTracesOnStart closes that, and
RoutingTraceRetention is exported so the daemon reads the same number the store
enforces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117tgnmbgZpHVV3XSNw8Qua
2026-08-06 19:20:24 +04:00
claude 7852aad60f every turn persists its decision record, and the reversal is written down (V-629)
internal/decision kept a 25-turn ring and persisted nothing, on the argument
that a turn record is read minutes later or never. The owner reversed that on
06-08-2026: the routing heads cannot be fitted or calibrated without real
utterances, and V-631 measured that 9 of the 31 modes have no seed example at
all. docs/plans/21-persisting-the-routing-trace.md carries the reversal, and
CLAUDE.md now says which of its own sentences stopped being true.

cmd/mavend/routingtrace.go is a second sink beside the ring, which did not move:
the ring is still what /trace reads and still what a test with no store gets. A
failed insert is logged and swallowed, because a trace must never change what he
hears. traceSink keeps a nil store out of the interface, since a typed nil
pointer there would pass the nil check and die on the first turn.

Four fields the ring never carried: which reach the turn arrived on, whether
stage 0 answered before the classifier was consulted, which encoder body was
live (the same EmbedderID string the vector marker uses), and what the action
stage actually did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117tgnmbgZpHVV3XSNw8Qua
2026-08-06 19:13:20 +04:00
claude 92de4ae496 Merge pull request 'Reconcile the seed labels with the handlers (V-628)' (#179) from task/633-reconcile-the-seed-labels-with-the-handl into master 2026-08-06 16:28:40 +02:00
claude c1d9a4547b Merge pull request 'Route with a fine-tuned e5-small instead of a generative model: three heads, no free generation' (#177) from task/546-route-with-a-fine-tuned-e5-small-instead into master 2026-08-06 16:25:51 +02:00
claude e7ecce2859 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
2026-08-06 18:09:11 +04:00
claude 2b3e34c7e8 label seeds with the stage 0 grammars and gemma, and measure both (V-546)
The plan calls the labeled set the whole project and names the stage 0
grammars as the label functions. cmd/labelgen runs them, the real ones in
buildRouter order, so a rule change moves the training data with it.

Gemma labels the rest at 334ms/call with nothing unparsed, which matches the
plan's estimate. It agrees with the seed files on 197/277, and reading the
disagreements is the finding: the seeds and the router prompt hold different
definitions of system, of a world question and of a bare verb. V-626.
2026-08-06 13:21:06 +04:00
claude 23ad5c0247 gofmt ecosystem_acts.go, so make test reaches the tests (V-623)
The struct field alignment drifted when the confirm's action id landed, and
fmt-check is the first gate in make test. Every branch cut since inherited a
red suite for a reason no branch owned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 11:47:27 +04:00
claude 12ecc30c57 Merge the ecosystem sweep: the wrong item, and an unjoined authorisation (#264)
Seven of the eight non-negotiable rules hold and were checked one by
one. The eighth, one correlation id per action, was violated across the
confirm boundary.

entityAttentionCapability.handle read items out and remembered none of
them. rememberSurfaced had exactly one caller, the unscoped digest. So
after 'что с muzick indexer' the positional memory still held the
previous digest, and 'отметь второй как сделанное' indexed into a list
he had not just heard, transitioning somebody else's Praxis item. That
is the precise harm the position resolver's own comment says it exists
to prevent.

A parked Hexis confirm did not carry the correlation id of the action
that proposed it. The confirm arrives on a later turn with its own
context, so execHexis read causationID as empty and minted a fresh one:
the nexus resolve, the capabilities call and the execution they
authorised landed in the trace as three unrelated calls, with nothing
joining the authorisation to what it authorised.

Both are the shape that found six bugs tonight. The first reports a
transition on the wrong object. The second reports an execution that
cannot be tied to its own authorisation.

(V-623)
2026-08-06 05:24:22 +04:00
claude 190cf0c794 the scoped digest remembers what it read out, and a confirm keeps its action's id (V-623)
Two ecosystem defects, both of the shape where a call reports done and
nothing of the sort happened.

entityAttentionCapability surfaced every item it spoke and remembered none
of them, so the previous digest stayed the positional memory. A follow-up
"отметь второй как сделанное" then indexed into a list he had not just
heard and transitioned somebody else's item, which is the exact harm the
position resolver exists to prevent.

A parked Hexis confirm did not carry the correlation id of the action that
proposed it. The confirm lands on a later turn with a context of its own,
so the execution recorded a fresh id and an empty causation: the resolve,
the discovery and the thing they authorised sat in the trace as three
unrelated calls. The contract mints one id per action.
2026-08-06 05:23:31 +04:00
claude d156be3442 Merge the rest-of-day cap (#262)
Asking "что дальше?" at 04:45 read all 43 entries of the day aloud. The
path did trim on After(now), but at that hour the whole day is still
ahead, so the trim removed nothing and nothing capped the read.

The cap is three. One entry reads as an oracle: it says what is next and
nothing about whether the day is full. Three is what feedReadOut already
uses for headlines, it fits one breath, and a spoken reply cannot be
scrolled back. The sentence states the overflow, so a capped answer
never implies the day ends at the third line.

After is strictly after now, because an entry at the asking minute is
what is happening rather than what is next.

"что у меня сегодня" was never on this path. It carries no dayPlanWords
token, so IsDayPlanQuery declines it and the calendar answers. That
separation is pinned now rather than assumed.

Conflict in dayplan_test.go resolved by keeping both tests. Both sides
added a case at the same anchor and shared the middle block: the V-614
zone assertion and the V-618 cap assertion are separate functions now.

--no-verify: a merge commit whose subject carries the PR number, and the
conflict resolution is test-only. Full race suite exit 0.

(V-618)
2026-08-06 05:12:20 +04:00
claude 10975eff07 "что дальше?" answers with the next three, not the whole day (V-618)
Measured on the box at 04:45: "что дальше?" read 43 entries, 05:45 to 21:12, as
one spoken sentence. The rest-of-day path already trimmed to what had not
happened yet, and at 04:45 that trim removes nothing — the whole day is still
ahead. Trimming was never the narrowing; nothing capped the read.

Plan.Next(now, n) is After with a cap, and the overflow is counted rather than
dropped. The cap is three. One entry is defensible and reads as an oracle: it
says what is next and says nothing about whether the day is full. Three is what
the feed already reads back for headlines, it fits in one breath, and the reply
is spoken — he cannot scroll it back. Above three the answer stops being an
answer and becomes a recital, which is the defect.

The sentence says whether more remains: plan_next is "дальше: …" and
plan_next_more appends "и ещё 40 дел до конца дня." So a capped answer never
implies the day ends after the third line.

After is now strictly after now. An entry at exactly the asking minute is the
thing happening, not the thing next.

"что у меня сегодня?" is untouched and was never on this path: it carries no
plan word, so IsDayPlanQuery declines it and the calendar listing answers the
whole day. TestWholeDayQuestionIsNotTheRestOfTheDay pins the two apart.

The empty case already said the right thing — plan_rest_empty, "на сегодня
больше ничего не запланировано", not the whole-day empty line that would deny a
day he just lived — and now has a test at the cap boundary too.

Routing fixture unchanged, 64/91 (70.3% full, 70.3% intent-only) before and
after: no router file is touched. Suite green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 05:08:48 +04:00
claude 62675e8fe4 Merge the spoken-plan zone fix (#259)
FormatRU printed the raw instant, so it read the plan's hours in
whatever zone the value carried. The live case is the rest-of-day path:
'что дальше?' rebuilds a morning.Plan off ipc.DayPlan, and nothing there
had put the instants in the asking clock's frame. It is the only
producer of a Plan that skips BuildPlan, which has localized events and
reminders since it was written.

formatTime, the answer to 'когда я это сделал?', had the same shape on a
fact's Ts, which is UTC out of the store.

Each test builds its instants three hours off the machine's zone, so
they fail under TZ=UTC as well.

(V-614)
2026-08-06 04:44:05 +04:00
claude 46acf3cba0 the spoken plan reads the clock on his wall (V-614)
FormatRU printed a plan item's At raw. An event and a reminder come off the
store as UTC — a calendar fact's Ts, a reminder's FireTs — while a checklist
line is built in the asking clock's zone, so one spoken sentence named two
zones. This is the voice path, so it is what he actually heard; the same
defect on /morning and /events was V-612.

Every hour is now read in the plan's own zone, Date's, which BuildPlan sets
from the asking clock. The rest-of-day path in queryDayPlan rebuilds a plan
off the wire, where nothing had put the instants in that frame, so it does
now.

formatTime is the same bug in the same daemon: "когда я это сделал?" names a
fact's Ts, and the branch that prints a wall clock printed the store's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 04:43:38 +04:00
claude e5158d8828 Merge the mavweb page sweep: three zone and form defects (#255)
Two pages rendered UTC where their siblings render local. /events showed
NoticedAt local and OccurredAt UTC in one row, on a page whose own hint
says that gap is meaningful. /morning showed a reminder at a different
hour than /reminders, which called .Local() on the same instant since
V-469. Both now .Local.Format.

/tasks read formWeight inside the due-date branch, so promoting a
candidate as srochno with no deadline discarded the importance and said
nothing. It is read unconditionally now.

Every table is already wrapped, every interpolation already escapes, and
the step-up gate is already on the mutating posts. Those were checked
and left alone.

(V-612)
2026-08-06 04:24:22 +04:00
claude 07f7550931 /events and /morning read the clock on his wall (V-612)
Three defects on the server-rendered pages.

/events printed both timestamps in whatever zone the value arrived in.
NoticedAt is the bus's local instant; OccurredAt is the store's UTC, or a
pubDate internal/rss parsed to UTC. So one row carried two zones and a feed
item read hours older than it was, on a page whose hint tells him that column
gap is real.

/morning printed a plan item's At raw. It is a calendar fact's Ts or a
reminder's FireTs, both UTC out of the store, so the same reminder named a
different hour here than on /reminders — which does call Local, since V-469.

promoteCandidate read the importance select inside `if due != nil`.
Confirming a candidate as "срочно" with no deadline threw the word away and
the row came back normal with nothing saying why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 04:23:46 +04:00
claude 6923a983aa Merge the k-preposition hour, and refuse an unresolved minute (#254)
V-610: teaching #252 that 'к' names an hour made HasTime true without
making the value resolvable, so 'напомни завтра к трём часам дня'
committed at the current clock. dateparser joins a day word to a clock
through 'в' and no other Russian preposition, so it read the day and
dropped the hour. The rewrite now normalises к, ко, на, во to в, which
also fixes 'напомни завтра на 9', silently broken the same way.

The durable half is ResolvedTheHour: both gates that read NamesAnHour
now refuse a parse whose minute nobody spoke, rather than defaulting to
the current clock. Same class as V-577. Fixture unmoved at 64/91.

(V-610)
2026-08-06 04:12:29 +04:00
claude 1d10c9535c The hour after "к" is read, and an hour nobody read is asked about (V-610)
"напомни завтра к трём часам дня позвонить врачу" now sets 15:00. It set 03:53,
which was the clock at the moment of the turn. She confirmed that as the hour he
had just said.

#252 taught hourPrepositions and the dateparser rewrite the preposition "к". So
HasTime and NamesAnHour started answering true for the sentence. The value did
not follow. The rewrite kept his preposition and handed dateparser "завтра к
03:00 pm". dateparser joins a day word to a clock through "в" and through no
other Russian preposition. It read the day, dropped the clock and filled the time
from its relative base. The completeness rule then saw what, time and day all
answered, and committed at the current minute.

The preposition is normalised along with the hour now. "на" was losing the clock
the same way and was never measured. So "напомни завтра на 9" was landing on the
current minute too.

The second half is the durable one. ResolvedTheHour is the gate the reminder slot
reads, and it refuses a parse whose minute nobody spoke. A spoken hour lands on
the hour. The three shapes that name a minute of their own are a written clock, a
half hour and a quarter to. Anything else came off the clock the parser was
handed. An interval is exempt, because it lands where the arithmetic says.
Comparing the whole instant to now is the obvious test and it is wrong.
ru-rem-006 resolves to 12:00 and the fixture reference clock is 12:00. That is an
hour he did say, reading as an hour nobody did.

The five sentences measured on the box are pinned as tests. They run against the
stub and against the production parser, and the two that already passed are in
there too.

Fixture unchanged. classifier+hash is 27/91 and classifier+onnx is 64/91, before
and after. reach is 18/30 and 27/30, before and after. No case moved and no
clarify count changed. Suite green under -race.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 04:11:47 +04:00
claude dd699b706f mavpoll refuses the flags that would kill it later (V-611)
Three ways a mavpoll process died or lied after start:

- -interval 0 panicked time.NewTicker on the first tick.
- -timeout 0 is 'no deadline' to http.Client, so one wedged source
  stalls every source behind it forever.
- -wg-cmd '' indexed field 0 of an empty slice in pollWg.

All three are now refused in run(), where the operator reads the
message, and pollWg guards its own command as well.

A body that hits maxBodyBytes was silently truncated. A cut kuma page
parses cleanly up to the cut and every monitor past it looks deleted,
so the poller would write 'unknown' over live services and the
down-rule would go quiet. Read one byte past the cap and refuse.

wg's stderr was dropped by Output(), leaving 'exit status 1' in the log
where the real cause is a missing CAP_NET_ADMIN or a bad interface.
2026-08-06 04:03:48 +04:00
claude 9e1958e7b0 one weekday matcher, and a stem list stops answering for sredstvo (V-581)
router.WeekdayIndex reads the lexicon and asks the dictionary about the case.
Four private lists go away: the habit declension map, the weekday block of the
day-plan refusal, the weekday and part-of-day entries of the weather guard, and
the stem list in ruwords.go.

The stem list was the real defect. mentionsUnknownDay matched sred, pyatnits
and subbot with strings.Contains, so sredi, sredstvo and sredniy all read as
Wednesday and a question carrying one was answered with onlyNearDaysReply
instead of a date. It matches whole tokens now.

The weather guard was a third copy of three closed sets that already exist.
It kept the rooms of the house, which are its own, and asks the lexicon for the
weekdays, the parts of the day and the words that follow v without naming a
place. Questions phrased v srede, v utra and v obshchem reached the geocoder as
cities before.

Full suite green under -race.
2026-08-06 03:33:01 +04:00
claude 04584fb2da webfetch checks the status before it reads the body (V-581)
A non-2xx reply was read in full first and only then rejected. Two costs
followed. A 500 with a large error page pulled up to MaxBytes off the wire for
nothing. An error page over the cap returned ErrTooLarge, which names the size
and hides the status the server actually sent.

The status is a typed error now. webfetch.StatusError carries the code and
unwraps to ErrStatus, so errors.Is keeps working and errors.As reads the number.
crawl.StatusError is the same shape on the other side of the seam, and
cmd/mavend/crawls.go carries the code across.

That removes the string grep in crawl.isServerError, which decided whether a
failed robots.txt blocks a crawl by looking for " 50" in an error message it did
not own. A reworded error would have turned a 503 robots.txt into permission to
crawl. It reads the code now.

Two comments corrected. webfetch.HostMatches said the crawler calls it and
nothing outside the package does. rss.PlainText said the crawler's extractor
goes through it and crawl/extract.go has its own pass.

The rss poller parses the feed straight off the byte slice instead of copying a
document that can run to a megabyte through a string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 03:12:59 +04:00
claude 316fb197a8 Merge the voice daemon sweep: four named values, one dead import block (#239)
Read all of mavsttd, mavttsd, mavwaked and mavenclient. What changed is small
and behaviour-preserving: whisperThreads and noSpeechFloor named in
whisper_handler.go, piperSampleRate and targetSampleRate named in
piper_handler.go where 22050 and 16000 were repeated four times across the
resampler, and mavenclient/main.go lost four imports kept alive by var _ lines
for helpers that never arrived.

Three things the sweep checked and found already right, which is why they are
worth recording. mavwaked's header says it has no wake-word model, which is
true and matches V-487 rather than being a drifted comment. whisperHandler.Close
does not race an in-flight Transcribe, because worker.Server.Close closes the
listener and then waits on the group before main's deferred Close runs. No
subprocess, pipe or CGO context leaks on an error path.

defaultSocket is genuinely duplicated between mavsttd and mavttsd and stays
that way: folding it means exporting an unexported config helper, which is a
larger change than this sweep's scope.

The agent refuted the brief's prediction of swallowed errors and leaked
handles. This file set had magic values and dead code instead.

(V-581)
2026-08-06 03:03:51 +04:00
claude 67decc42f0 sweep: name voice-daemon magic numbers, drop dead keep-alive vars (V-581)
mavsttd/whisper_handler.go: name the no_speech_prob confidence-zeroing
floor (0.9) and the whisper thread count (4), both previously bare
literals with no reason attached.

mavttsd/piper_handler.go: name piper's render rate (22050) and the
canonical wire rate (16000) used by the resampler, instead of repeating
the two numbers inline four times.

mavenclient/main.go: remove the strconv/io/net/time imports and their
`var _ = ...` keep-alive lines — dead weight with no caller, not future
scaffolding.

Behaviour-preserving; no test changed. go test -race ./internal/...
./cmd/... is green.
2026-08-06 03:00:21 +04:00
claude 201fe03d20 Merge the invented note and the repeated ask (#238)
V-592 is a phrasing defect. The store was never wrong: DefaultFactParser files
я выпил воды as key=water value=drank, and no стакан reaches the index. The
glass was copied out of the prompt. ReplySystemPrompt's example was literally
'Записала, что ты выпил стакан воды', replyContext hands the model
'записала факт: water "drank"' with no Russian to work from, and the nearest
plausible sentence in context was the example itself. выпел is the 1.7B
garbling the verb.

So the fact path stops generating and echoes, per V-576. The prompt example is
contentless now. Two smaller things fell out: the stub read the parser's key
back at him as 'отметила: water = "drank"', and a fact clarified out of запиши
confirmed as запиши, because a fact answer fills no Text slot.

V-593: whenKnownOf reads the three things he must say off the same predicates
whenGapOf uses. An answer that moved any of them forward puts 'Поняла: <his
words>.' between the clock and the question. An answer that moved nothing
repeats the question unchanged, which is honest. The acknowledgement echoes and
never restates, for the same reason as V-592.

A new differs field on a trace turn fails a byte-identical consecutive reply.

Open for the owner: whether the clock repeats on every ask of one flow. He
ruled that she states the time, not that she states it on every question.

(V-592) (V-593)
2026-08-06 02:59:29 +04:00
claude fec572c997 a re-ask names what the answer before it gave her (V-593)
After "на 9" and then "на завтра" she asked "Сейчас 02:34. Это утра или
вечера?" twice, byte for byte. Asking again is right — the half of the
day is still unsaid — but a reply with no trace of his turn in it is
indistinguishable from not having been heard, which is the failure mode
the V-558 family exists to remove.

whenKnownOf reads the three things he has to say about the time off the
same predicates whenGapOf reads. When his answer moved any of them
forward, the ask carries an acknowledgement of what it took, in his own
words and never a restatement: a 1.7B asked to say a Russian sentence
back is exactly where V-592 came from. When it moved nothing, there is
nothing to acknowledge and the question repeats honestly.

The clock still opens every time question, per the owner's ruling. The
acknowledgement goes between it and the question. Whether she should
state the clock on every ask of one flow is his call, not mine.

Also folds a fact's raw answer into the parked utterance (V-592): a fact
fills no Text slot, so "запиши" + "пил воду" confirmed as "запиши".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 02:58:49 +04:00
claude 5187f3bd14 a captured fact is confirmed in his words, not the model's (V-592)
"я выпил воды" came back as "Проверила, что ты выпел стакан воды". The
verb is not a Russian word, the glass was never mentioned, and nothing
had been checked.

The store was right throughout: DefaultFactParser files this as
key=water value="drank", and no row anywhere held "стакан". Every
Russian word in that sentence was generated. replyContext hands the
model "записала факт: water \"drank\"", so the model had nothing to
phrase FROM and reached for the nearest plausible sentence — the example
in ReplySystemPrompt, which was literally "Записала, что ты выпил стакан
воды."

So the fact path stops generating, the way the note payload did in
V-576. The confirmation is a fixed deck frame with his own sentence in
it, in both repliers, and the prompt example is contentless now. The
stub also read the parser's KEY back at him, which is machine
vocabulary he never said.

The clarify half of this — a fact clarified out of "запиши" answers with
"запиши" and nothing else — lands with V-593, which touches the same
lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 02:58:37 +04:00
claude 502327678f a Praxis lifecycle failure names Praxis (V-588)
praxisItemAction.handle returned a hardcoded per-verb constant on any error, so
a Praxis outage, a refused token, a contract mismatch and a decode failure all
said the same thing and none of them said "Praxis". The information already
existed: praxisClient embeds ecosystemHTTP, so the error is an *ecosystemError
with working classifiers, and handle() logged it, traced it and threw it away.

servicePraxis joins the two service constants and the failure goes through
ecosystemGap, which is what Nexus and Hexis already use. The per-verb string is
kept in front of it rather than replaced: it carries which operation did not
happen, and the trace is the only other place that exists. No new Russian is
added — both halves are lines that already ship.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 02:49:09 +04:00
claude 5d2fd91c06 a Hexis 401 says the token was refused, not that Hexis is down (V-587)
The vendored Hexis client is a separate implementation and returns a plain
fmt.Errorf for every status at or above 400, so errors.As for *ecosystemError
never matched, Unauthorized() was never consulted, and ecosystemGap always fell
through to the outage line. A wrong token sent him to inspect a healthy service.

hexisError classifies at Maven's boundary, since the client is vendored from
another repo and a local edit there is lost on the next re-vendor. The status
text is the only signal that survives the wrapping, so that is what it reads;
anything unrecognised stays at status 0, which is what Unreachable() means. The
correct fix is a typed error upstream carrying the code, and Maven cannot land
it unilaterally.

execHexis is the second site and it did not call ecosystemGap at all. It now
does, but only for a failure that belongs to the service. An execution that Hexis
accepted and that then failed keeps the command-level line: that is the command
failing, not Hexis degrading, and calling it an outage would be the same defect
pointed the other way. Authorization is unchanged: a 401 is still a refusal, it
is not retried and nothing proceeds on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 02:48:00 +04:00
claude be18649953 Merge the caldav and maild sweep (#232)
One shared response-body read cap named. Both daemons wrote 4<<20 as a bare
literal in two files with no reason beside it.

The rest of the brief was refuted. No swallowed error, no drifted comment, no
repeated connect block. The IMAP password is read once from a file, never
logged, and never crosses to core: mailIngester has one method and it takes
mail content. internal/email/imap.go names no STORE, Seen, Move or Delete verb,
so read-only holds.

(V-581)
2026-08-06 02:32:51 +04:00
claude 188d9fc02f mavcaldav: name the shared response-body read cap (V-581)
fetchEvents and listPublished both bounded their HTTP body reads at
4<<20 with no name for what the number was for. One constant,
maxResponseBody, documents the reason (cap every CalDAV response this
daemon reads) once instead of twice. No functional change.
2026-08-06 02:26:42 +04:00