queryGeneral has nothing fetched to fall back on, so it is the sharp case:
with a workstation configured and asleep he is told that, rather than told
something false in a confident voice. The 1.7B answering a world question is
where "Война и мир" got Левитан as its author.
The sources that already hold a passage — a live search, a ZIM article, a
page he named — go through the world model too, but read the passage back
when it is not there instead of naming a gap. A real quote beats "не могу
сейчас", and nothing is invented on either path.
The Stub and every test double keep the Phraser interface they have.
PhraseWorld is reached by assertion, and a phraser without it is the
no-workstation case.
The middle outcome is the whole task: a workstation that is configured and
asleep produces a gap, and the resident model is never asked. The parity
test compares the bytes PhraseWorld sends the workstation against the bytes
PhraseQuery sends the resident model, so the fixtures and the daemon cannot
measure two different prompts.
The nudge tests cover the silent half from both sides, including the
temperature, which is how the workstation would otherwise change how she
sounds without anyone deciding to.
The naming half of the degradation rule in docs/offload.md. PhraseWorld has
three outcomes: no workstation configured means the resident model answers
exactly as today, a workstation that is taking work answers, and one that is
asleep returns ErrNoWorldModel so the caller can say so. Naming a gap
requires a gap — on a box that never had a second model, refusing every
world question would remove a capability he has now.
Both prompts move into knowledgePrompt and evidencePrompt, shared by
PhraseQuery and PhraseWorld, because prompt parity across two models stops
holding the moment there are two copies of a prompt.
The silent half comes with it: chatWithSystem and chatWithMessages prefer
the workstation when it will take work, at the same 0.7 the resident
transport samples at, and say nothing when it will not. That covers the
digestion worker's nudge and reminder phrasing without touching tick.go.
Only Available and CompleteRemote are in the Remote interface. Pair.Complete
has its own floor and the phraser already owns one; two floors under a
single call is one too many.
The phraser's own transport has always sampled at 0.7 and this client has
always been greedy. Routing a phrasing call through the client must not
change how it decodes, so Req carries the temperature and 0 — the zero
value, and what every existing caller wanted — is still greedy.
Both fixtures, run from homesrv across the LAN with the proxy env stripped.
Routing: 84.4% full / 93.5% intent-only at p50 329ms through the cascade, against
72.7% / 77.9% at p50 0.80-1.04s for Qwen3-1.7B. Talk: 25/27 against 20/27, with
knowledge 9/9. Nudges 15/15. Settles #485's first assumption by measurement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJYcaBiuny9UGSpFweQVQ1
modelSeam builds an llm.Pair when a workstation is configured and hands it to
the router and the replier. Both are the silent half of the degradation rule:
the big model is only better there, and he is never told which model answered.
No block, no probe, and the box behaves exactly as it did.
Owner's call, 02-08-2026. Without it the 12B spends the reply budget on
reasoning tokens and answers empty at low max_tokens. Verified on the box:
"Столица Франции?" now answers "Париж" with no reasoning_content.
make build now catches a broken supervisor on homesrv. deploy/mavgpud.json
carries the owner's gemma-4-12b line with the MTP draft model, passed to
llama-server untouched. The unit is a systemd user unit because sudo on the
workstation wants a password; lingering is the one command left to the owner.
The fixtures are the live numbers sampled from the box on 02-08-2026, where the
CPT run held 12.8GB of 16 as proc/478104/vram_35881.
The cases that matter are the ones where a mistake is silent: our own
llama-server counting as a contender, an unreadable card reading as free, and
/health hanging or proxying into a closed port instead of answering 503.
The lifecycle rule from Vikunja #488. Not on demand, because a 7-14B takes tens
of seconds to load and a world question would meet a gap every time the card
had been quiet. Not always on, because that is what holds the card.
/health is answered locally and always, so Maven's prober costs nothing and
works while the model is down. Everything else is reverse-proxied to
llama-server, which is what makes the idle window measurable at all.
Yielding is checked before starting, and both transitions are damped by a poll
streak so a short-lived rocm process cannot evict the model.
The workstation cannot keep a 7-14B resident: it would hold 16GB against the
owner's CPT runs, Correx and the manga-recap pipeline. So the process that
stays up costs no VRAM and the model comes and goes under it.
Contention is detected by presence on the KFD, not by a VRAM threshold. A ROCm
process registers under /sys/class/kfd/kfd/proc when it initialises HIP, well
before it allocates, so we see a contender during its startup instead of after
it has already lost an allocation race. rocm-smi is not installed on that box
and a per-second subprocess would get tuned down until useless, so this reads
sysfs and forks nothing.
Free VRAM is read only to decide whether to start. It is never a reason to
stop: by the time free VRAM has dropped, the other job has already failed.
The remote model cannot be a llama-server that is simply left running: a
resident 7-14B holds 16GB against the CPT runs the card is for. So what
is always up on the workstation is a supervisor, and llama-server is
loaded while the card is free.
Still not a scheduler. It arbitrates nothing between callers, and Maven
never asks it to start anything.
Seven cases. The load-bearing ones are the constraint from 483: an
unconfigured deploy never probes and always reaches the floor, a busy
card degrades silently with the remote untouched, and a remote that dies
between probes still completes the turn and corrects the cached answer on
its way out.
CompleteRemote is pinned not to fall back, because a named gap that
quietly became a 1.7B guess is the failure this whole split exists to
prevent. And 1000 Available calls are pinned to make zero probes.
Pair holds both models and decides which answers. A prober asks the
remote whether it will take work and caches the answer, so a request
reads an atomic bool rather than paying for a health check. Routing sits
at p50 825ms on the hot path and must never wait on a machine that may be
asleep.
The two methods are the two halves of the degradation rule in
docs/offload.md. Complete falls back silently, for routing, replies and
nudge phrasing, where the big model is only better. CompleteRemote
returns ErrRemoteUnavailable instead, for a world question, where the
1.7B does not answer worse but invents.
A nil remote is the unconfigured deploy: nothing probes, everything goes
to the floor, and the box behaves exactly as it does today.
483 is an umbrella and its children are the work, so what it owes them is
the shape they must all obey. docs/offload.md records it: the degradation
rule and where its line falls, admission control rather than a GPU
arbiter, the embedder staying on homesrv because it backs the classifier,
and the inventory of what runs a model on the box today.
CLAUDE.md gets a pointer, because an agent about to add a model caller or
touch a daemon seam needs to know this before it starts, not after.
It was untracked but not ignored, so one git add -A would have committed
MAVEN_AMBIENT_TOKEN. Same class as deploy/telegram.env, which is already
ignored.
Five hardcoded transports, three in internal/ipc and two in
internal/worker, all now go through the seam address. The unix perms
logic moved into netaddr, so the two copies of parentDir and the umask
dance are gone.
peerCaller already returned ok=false for a non-unix conn, so the
SO_PEERCRED path degrades correctly on tcp with no change.
Six cases. The load-bearing one is the first: every deploy in the tree
writes a bare path, and it must keep meaning a unix socket with no
handshake in front of the payload.
The rest cover the tcp seam: a good token round-trips, a wrong one comes
back ErrUnauthorized, a stranger that speaks HTTP at the port is dropped
while the listener stays up for the next peer, and a tokenless tcp bind
fails rather than serving his turns to anyone who connects.
internal/netaddr parses a daemon seam address and dials or binds it. A
scheme-less address is unix and behaves exactly as it does today: same
0700 parent dir, same 0600 socket, same bytes on the wire. tcp://host:port
is the new option, and it is what lets a module live on another host.
Over tcp the filesystem permission that authenticated the unix socket is
gone, and what crosses this seam is audio of the owner speaking. So a tcp
listener requires a shared token, checked in constant time before the
first protocol frame is read, and a peer that fails is dropped without
taking the listener down with it.
morning_routines, feeds, crawl.on_demand and netscan.enabled in
deploy/mavend.json; -ambient-token on mavweb, interpolated from a gitignored
/.env. All verified on the box: the dispatcher builds the morning plan,
/api/ambient answers 401/201, the crawler reads a named page, netscan finds 3
devices, and /events fills with scan:lan and ambient:notif.
Filed 482 (ambient reads a notification's wall clock as UTC). Corrected 479:
both capabilities work once configured, so it is not a routing defect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJYcaBiuny9UGSpFweQVQ1
Ran sessions 1 and 2 on the live box.
Session 1 steps 1 and 3-6 pass. Steps 2 and 7-9 need a person at the box.
POST /api/chat is drivable with form encoding and a cookie jar, so the text
half needs no browser.
Session 2 confirms the deploy matches the bench at 72.7% full accuracy, and
contradicts two recorded numbers. The classifier scores 68.8% at p50 16.6us,
not 36.8% at 31ms. Router latency measured under contention again.
Also: 319 item 2 point 2 closes on the recall margin sweep, CheckFeminine has
a false positive on second-person masculine verbs, and the wake path cannot be
checked because mavwaked and mavenclient are deployed nowhere.
The plan named 40 task numbers on 2026-08-01. Ten open QA tasks were missing
and two of the named ones had closed, so the 44-of-50 header was wrong twice
over.
- header is 42 of 50, and every open task now appears
- placed the ten unlisted QA tasks: 14, 248, 249, 250, 258, 283, 284, 285,
286, 323
- new Operations sitting for 249 and 250, and a Query sources sitting for
258 and 286
- 14 and 284 join housekeeping: both are gated on something unbuilt
- dropped the 317 and 354 rows, closed 01-08-2026, with one line saying what
landed
- 319's gate recalibration is done; what is left is re-deriving QueryMinMargin
- 323 is down to the 60s startup timeout arm after PR #90
- new "Not this repo" section for 358 (Hexis) and 362 (training workspace)
- router latency is ~27x, not 90x; the 2.7s p50 was contention, not the model
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJYcaBiuny9UGSpFweQVQ1
Every phraser test built the phraser with NewLLMPhraserAt, which starts no
process, so NewLLMPhraser, spawnLlamaServer, startLlamaProc, llamaProc.Close
and extractPort sat at 0% while the package headline read 65.3%.
These drive the real spawn code against a fake llama-server script: the port
scrape, the three reachable startup-race arms (start failure, stderr EOF,
context cancel), and Close actually reaping the child. The orphan test
re-execs the test binary as the daemon, SIGKILLs it, and asserts Pdeathsig
killed the grandchild. The last test rebuilds the production command line and
checks kill-maven.sh's pattern still matches it — that pattern has gone stale
twice and leaked orphans both times.
Package coverage 65.3% -> 76.9%. The 60s timeout arm stays untested; it needs
an injectable clock in production code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012YQGVXu5J1iCMCff5J4S1R
#400 rewrote the chat and query prompts in Russian and left three pieces
of English prose behind.
PhraseReminder's user prompt was fully English. It is Russian now, and it
no longer restates the JSON contract or the persona rules: the call goes
through chat(), so nudgeSystem already states both, and a second copy of a
contract is one more thing that can drift out of step with the first.
querySystemPrompt and router.KnowledgePrompt both closed with the English
"Respond ONLY with valid JSON:". That sentence is prose instruction, not
wire format — the JSON skeleton after it is the wire format, and it is
unchanged. Kept rather than deleted: the GBNF grammar makes it close to
redundant, but the grammar is switchable off (phraser NoGrammar), and the
sentence is the floor when it is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CotYKycuio1GwLbYh9jfc
Accepting a proposed routine gives the tick loop a standing new reason to
speak. DESIGN.md § "surface caps authority" puts that at layer 3, and says
voice is structurally incapable of layer 3 because a room mic is reachable
by anyone in the room. The /routines button was gated at step-up; the voice
path accepted outright. The two surfaces disagreed, so one of them was wrong.
A spoken "да" now leaves the row 'proposed' and sends him to /routines,
where the gated button is. A spoken "нет" still dismisses: declining does
not move the boundary outward, so voice keeps it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CotYKycuio1GwLbYh9jfc
dialogue.Slots gained Value in 925ce22, but toDialogueSlots never copied
it, so a clarifying answer carrying a fact payload still landed nowhere:
clarify.go:202 sends the answer through the converter, and the SlotValue
arm reads answer.Value.
Both converters now carry every field. TestSlotsParity compares the two
field sets by name and type; TestSlotsRoundTrip populates every router
field and checks the round trip, and fails the fixture itself when a new
field is left zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QChoBS5qJSrCV98oNUnHNU
Checked the doable, epic and infra tiers against the code, not just the two
tiers V-447 asked about. Ten entries are already built. The two the ranking
calls blockers for everything below are among them: sqlcipher at-rest ships as
Store.enc plus OpenEncrypted, and mavweb/mavcaldav have nine test files
between them where the ranking says zero coverage.
Also built and still ranked as work: rule trace, recurring reminders (cron +
RescheduleReminder), stale-reminder burst collapse (collapseReminders),
revert (VoidLatestFact), digest mode, testing infra, passkey persistence.
Recorded as a section at the top of the archived file so the tiers underneath
are read with the corrections in hand. No tasks created: V-447 scoped task
creation to the mandatory and easy tiers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xwomr2cT93KMSz9u3digX5
Checked the mandatory and easy tiers against the code instead of trusting the
2026-07-03 ranking. Two were already built and their tasks closed unstarted:
quiet hours (QuietHoursConfig + the care gate in internal/loop/loop.go:37) and
schema migrations (internal/store/migrations.go on PRAGMA user_version, 12+
steps shipped).
Three more were narrowed to what is actually missing. Destructive-confirm has
a mechanism and no policy: store.Tool.Destructive is one boolean, not a risk
tier. Bounded follow-up state has dialogue.Session with a TTL and slot
inheritance; what it lacks is Candidates, so "второй" resolves against nothing.
Clarify has a gate that can ask and one hardcoded sentence to ask with
(internal/voice/replier.go:56, which replier_llm.go hands straight through).
The other six were confirmed absent: list_items, capability model, go.mod
tidy, conversation repair, command history, pronunciation dictionary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xwomr2cT93KMSz9u3digX5
PROGRESS.md and 20-07-2026-BACKLOG.md were state snapshots that git log and
the Vikunja board already carry. Everything PROGRESS.md claimed as shipped is
a QA task. The backlog's only untracked item, bounded follow-up state, is now
V-448.
maven-feature-ranking.md moves to docs/archive/2026-07-03-feature-ranking.md
instead of dying. Its mandatory and easy tiers became V-449 through V-458; the
doable and epic tiers are reasoning about why things are not worth doing yet,
which no task captures.
Four code comments and the design.md ledger pointed at the deleted files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xwomr2cT93KMSz9u3digX5