28c2ffb84f
Reference-count the process-global ONNX Runtime across embedder and routing-head sessions, make close idempotent, and require named proof that both aggregate routing gates executed rather than self-skipped (V-716). Owner explicitly requested direct commits to master.
771 lines
43 KiB
Markdown
771 lines
43 KiB
Markdown
# QA plan: checking Maven properly
|
|
|
|
*Last verified: 2026-08-05 @ 12667fd. Living doc: correct it in place, do not append.*
|
|
|
|
Written 2026-08-01, after the 35-PR stack landed and the box came back up.
|
|
Refreshed 2026-08-02 against the live list, after PRs #85-#90.
|
|
Reconciled 2026-08-04 against the board, after the review stack merged.
|
|
|
|
The board holds 95 open tasks and 35 of them are `QA:` tasks. The ratio moved
|
|
because the build backlog grew, not because verification shrank. QA is
|
|
verification work, not build work, and most of it sat unverifiable while Maven
|
|
was down for 11 days. That blocker is gone.
|
|
|
|
Every open `QA:` task appears below. Distrust the count in this header first. It
|
|
is right on the day it is written and wrong a week later.
|
|
|
|
Fourteen ids this plan used to name closed on 2026-08-04 and are gone from it. If
|
|
you cannot find one, check whether it closed before assuming the plan dropped it.
|
|
|
|
This plan orders them by what unblocks what. Do sessions 1 and 2 first. Almost everything
|
|
downstream assumes the voice loop works, and nobody has confirmed that since
|
|
the redeploy.
|
|
|
|
---
|
|
|
|
## What the 02-08-2026 run found
|
|
|
|
Sessions 1, 2 and 3 all ran. Read these five before picking anything up.
|
|
|
|
- **470: a question writes invented knowledge into memory.** Recall then serves
|
|
it back. `что дальше?` lands on `IntentFact` and stores the model's answer as a
|
|
`self` fact at confidence 1.00. Two junk rows then claimed seven unrelated
|
|
world questions through recall, outranking the search leg. A question about the
|
|
capital of Australia was answered `какая последняя версия языка Go?`. Two bad
|
|
writes silently disabled world answering, with nothing logged.
|
|
- **466: a pending clarify is global.** One unanswerable clarify swallowed the
|
|
next three utterances from three separate sessions. With ntfy, telegram and
|
|
voice all live, a clarify raised on web chat eats the next telegram message.
|
|
- **467: spoken task capture is dead.** The router calls the capture marker an
|
|
`act`, and capture is reachable only from the `note` intent.
|
|
- **The classifier baseline in this repo was wrong**, and it flattered the
|
|
router. See session 2 and **464**.
|
|
- **477: the model swap and the self-update cannot be triggered on this box.**
|
|
Both are built and both are correct in test. The swap needs a passkey and
|
|
WebAuthn is unconfigured. `mavupdate` needs to reach a socket that only an
|
|
in-container uid can open.
|
|
|
|
- **479: an unconfigured capability lets the question escape to web search.**
|
|
Netscan off, asked `какие устройства в сети?`. She answered from the live web
|
|
with a general article about network hardware. A question about his LAN went to
|
|
an upstream engine. The crawler fails the same way.
|
|
|
|
Twenty-one defects were filed on 02-08-2026: 462 through 482. Six tasks this plan
|
|
had written off as blocked turned out to be ready to check. All six ran. Every
|
|
one of them is code-correct and stops at the deploy.
|
|
|
|
Three of the five config blockers in **472** were then cleared. The morning
|
|
routine, ambient ingest, feeds, the crawler and netscan are all live. Two remain,
|
|
and both are the owner's call: a token for each ecosystem sibling, and seed data
|
|
in Nexus and Praxis.
|
|
|
|
---
|
|
|
|
## Before you start
|
|
|
|
Two things bite anyone running these checks on homesrv.
|
|
|
|
**curl needs `--noproxy '*'`.** The shell exports `http_proxy=http://127.0.0.1:18080`.
|
|
Without the flag, every local check returns 503 from the proxy and looks like a
|
|
dead service. This cost me a false regression report today.
|
|
|
|
**The database is not readable with sqlite3.** Four older QA steps say
|
|
`docker compose exec mavend sqlite3 /data/maven.db "select ..."`. That cannot
|
|
work: the container has no `sqlite3` binary, and the store is AES-256-GCM at
|
|
rest with a tmpfs working copy. Read state through mavweb instead, at
|
|
`/history`, `/trace`, `/routines` and `/dash`.
|
|
|
|
---
|
|
|
|
## Session 1: the voice loop (half a day)
|
|
|
|
Nothing here has been confirmed since the redeploy, and everything else assumes
|
|
it works. Do this first.
|
|
|
|
Closes or advances: **44** (conversation), **45** (text chat), **287** (voice
|
|
session quality), **321** steps 3-5 (quiet mode), **288** (STT golden audio).
|
|
|
|
**288 is not blocked.** The fixtures are committed under `cmd/mavsttd/testdata/`
|
|
and `make test-stt-golden` runs today. This plan said otherwise until 02-08-2026.
|
|
|
|
Steps 1 and 3-6 were run on 02-08-2026 and pass.
|
|
|
|
**Step 2 no longer needs a person, and step 9 has a number now** (04-08-2026).
|
|
`POST /api/ptt` takes raw PCM16 16kHz mono and answers with audio plus an
|
|
`X-Reply-Text` header, so the committed STT fixtures stand in for a microphone:
|
|
|
|
```sh
|
|
tail -c +45 cmd/mavsttd/testdata/ru_query.wav > /tmp/q.pcm
|
|
curl -s --noproxy '*' -D /tmp/h -o /tmp/reply.pcm -X POST \
|
|
http://127.0.0.1:9201/api/ptt --data-binary @/tmp/q.pcm \
|
|
-H 'Content-Type: application/octet-stream' -m 180
|
|
```
|
|
|
|
That covers audio in → STT → router → phrasing → TTS audio out. It leaves only
|
|
browser microphone capture needing a person, and the wake path needing a machine.
|
|
Do not post `en_act.wav` without deciding first: it is a mutating act.
|
|
|
|
**Steps 7 and 8 still cannot run, but 15 is no longer the reason** (04-08-2026).
|
|
The desk presence poster is installed on workpc. It is a `maven-desk` systemd
|
|
user timer on a 60s cadence, gated by hypridle at 120s idle. `desk_active` facts
|
|
now arrive, and the first landed at 18:43.
|
|
|
|
What blocks the two steps now is that no rule wants to fire. `/trace` shows all
|
|
five at `predicate`, none inert:
|
|
|
|
| rule | sev | why it is false |
|
|
|---|---|---|
|
|
| water | 1 | needs ≥3h since the last `water` fact; step 2's `ru_fact` wrote one |
|
|
| meal | 1 | needs ≥6h since a `meal` fact; none exists |
|
|
| break | 2 | needs both `desk_active` and a `break` fact; `break` has never been written |
|
|
| service_down | 4 | no kuma monitor is down |
|
|
| netdata_critical | 3 | nothing critical |
|
|
|
|
So the honest way to run step 8 is to wait three hours after the last `water`
|
|
fact, or to write one antedated. Do not read the water rule's silence as a defect.
|
|
|
|
**The sev4 telegram reach works** (04-08-2026). Resuming a paused kuma monitor
|
|
for paperless, which is genuinely down, put a real `service_down` through the
|
|
whole path with presence away:
|
|
|
|
```
|
|
23:03 voicesink: no live voice session for service_down, falling through to away channels
|
|
/notifications: 19:03 | service_down | telegram | pending | Сервис перестал отвечать.
|
|
04.08 23:03 | nudge | service_down | telegram | sent | 23:03
|
|
```
|
|
|
|
`ChannelsFor(Sev4, Away)` returned telegram, the send succeeded, and the row
|
|
holds at `pending` because sev4 repeats until acked. The 15:51 row shows the
|
|
same rule reaching `acted` earlier, so the ack path works too.
|
|
|
|
The body was `Сервис перестал отвечать.`, which names no service. That is a bug
|
|
and it is deterministic, filed as **534**. `nudgeValues` fills `{service}` from
|
|
`State.Fact("service_down")`, an exact key mavpoll stopped writing when
|
|
per-monitor facts landed. Nine of the ten templates carry `{service}`, so all
|
|
nine are rejected as unfillable. The one nameless variant is left as the only
|
|
usable one, every time. The stub and LLM phrasers both call `loop.DownServices`
|
|
and get it right. The template path is the one that runs.
|
|
|
|
**Presence itself has a real defect, filed as 532.** `SavePresenceState` has no
|
|
caller outside tests, so the singleton row is never written. The gate is fine,
|
|
because it reads the bucket `GatherState` computes in memory each tick. Two
|
|
things follow. Hysteresis is dead, because `lastBucket` is always cold-start `Away`
|
|
and the 0.30-0.55 hold band never applies. And every presence readout lies:
|
|
`/dash` shows `away — score 0.00 (never)` with fresh `desk_active` facts arriving
|
|
every 60s. Do not trust that number while checking anything else here.
|
|
|
|
Steps 1 and 3-6 do not need a browser. `POST /api/chat` takes a form-encoded
|
|
`text=` field and a cookie jar, and answers with the rendered `/chat` page:
|
|
|
|
```sh
|
|
curl -s --noproxy '*' -c jar -b jar -L -X POST \
|
|
http://127.0.0.1:9201/api/chat --data-urlencode 'text=привет'
|
|
```
|
|
|
|
Parse the whole page, not the last text node. The page carries nav and footer
|
|
text. A naive tail of the Cyrillic nodes returns the wrong string, which makes
|
|
turns look misaligned when they are not.
|
|
|
|
1. Open `http://127.0.0.1:9201/chat` and hold a short conversation in Russian.
|
|
Watch for three things: she answers in feminine forms (`рада`, `поняла`), she
|
|
says `ты` and never `вы`, and no pet names appear.
|
|
**Passes** (02-08-2026, five turns): `я рада`, `поняла`, `помогла`,
|
|
`проверила`, `записала`, `грустна`, `ты` throughout, no pet names.
|
|
2. Press push-to-talk on `/dash`. Say `привет`. Confirm a spoken reply comes
|
|
back. This covers browser mic to STT to core to TTS as one path. It does
|
|
**not** cover the wake word or the voice-activity gate, and no step here
|
|
does — see below.
|
|
**Passes below the browser** (04-08-2026, three fixtures through `/api/ptt`):
|
|
HTTP 200, `audio/l16;rate=16000;channels=1`, and real speech back. `ru_query`
|
|
answered `на 04.08.2026 ничего нет.` in 3.82s of audio at RMS 3865, `ru_fact`
|
|
answered `отметила: water = выпил`, `ru_reminder` answered `хорошо, напомню.`
|
|
at `intent=reminder`.
|
|
**Passes in the browser too** (04-08-2026), and it needed no person. Headless
|
|
Chrome takes a fake microphone, so the whole browser half runs unattended:
|
|
|
|
```sh
|
|
chrome --headless=new --remote-debugging-port=9333 --remote-allow-origins='*' \
|
|
--use-fake-device-for-media-stream --use-fake-ui-for-media-stream \
|
|
--use-file-for-fake-audio-capture=cmd/mavsttd/testdata/ru_query.wav%noloop
|
|
```
|
|
|
|
Then drive it over the debug protocol: click `#btn`, wait, click again, read
|
|
`#status` and `#log`. That covers `getUserMedia`, `MediaRecorder`, the webm
|
|
decode and the hand-written resample to 16k Int16. It logged
|
|
`sending 188160 bytes`, which is 5.88s at 16k mono, and got the reply back.
|
|
|
|
**The button is on `/`, not `/dash`.** `handleVoice` serves it at the root
|
|
(`main.go:332`). `/dash` is the presence and fact dashboard and carries no
|
|
`#btn`. This step said `/dash` until 04-08-2026.
|
|
|
|
One defect fell out, filed as **533**. The reply logged as
|
|
`на+04.08.2026+ничего+нет.` The header is escaped with `url.QueryEscape`,
|
|
which writes a space as `+`, then decoded with `decodeURIComponent`, which
|
|
leaves `+` alone. Transcript only, the audio is fine.
|
|
3. Say `тихий режим`. Expect `тихий режим включён. буду реже напоминать.` **Passes.**
|
|
4. Say `выключи тихий режим`. Expect `тихий режим выключен.` Negation must win. **Passes.**
|
|
5. Say `в комнате тихо`. Quiet mode must NOT flip. Confirm on `/history` that no
|
|
`quiet_hours` fact was written. **Passes**: no row written. She answers `пока
|
|
не умею отвечать на этот вопрос.`, so it lands on `IntentSystem` with no arm.
|
|
6. Say `включи режим тишины`, then `сделай потише`. Both must flip quiet mode
|
|
on. These are the noun form and the comparative, added 01-08-2026. **Both pass.**
|
|
7. Wait for a nudge, then say `потом` within twenty minutes. Expect `хорошо,
|
|
вернусь к этому позже.` and the nudge row on `/notifications` reading
|
|
`snoozed`. Say `потом` again with nothing pending: it must route as an
|
|
ordinary utterance, not be swallowed.
|
|
8. Wait for the water nudge, then say `выпил воды`. Expect the ordinary fact
|
|
reply and nothing extra. She must not congratulate you. Check
|
|
`/notifications`: the row reads `acted`. Then trigger another nudge and say
|
|
`готово`. Expect `отлично, отметила.` and the same outcome.
|
|
9. Note anything where she is slow, cuts off, or talks over herself. That is
|
|
287's whole content and it has no written acceptance criteria yet.
|
|
**First evidence, in text** (02-08-2026): nothing breaks, but answers wander
|
|
and stitch unrelated topics. Asked whether he should move flats, she opened
|
|
with the weather. That is 287, and it is a phrasing problem, not a loop problem.
|
|
**The slowness now has a cause and a number** (04-08-2026). A spoken turn
|
|
takes 32 to 34 seconds. One phrasing call is 30.0s of that. STT is 1.0s
|
|
and routing is under 10ms. Both interactive calls decoded exactly 512 tokens,
|
|
which is the phrasing cap. Both were truncated, to produce a reply of
|
|
under 25 characters.
|
|
The cause is `responseGrammar`, not the model. Its last rule is
|
|
`ws ::= [ \t\n]*`, and `*` is unbounded, so the model emits `{` and then
|
|
satisfies `ws` with whitespace until `max_tokens` stops it. Reproduced on a
|
|
second server: at `repeat_penalty` 1.0 it runs to 512 and returns
|
|
`finish_reason=length`, at 1.3 it stops at 24. Bounding the rule to
|
|
`[ \t\n]{0,4}` gives a clean stop at 33 tokens three times out of three with
|
|
no penalty at all.
|
|
Only some callers are exposed. `internal/llm.Req` sends `repeat_penalty` and
|
|
the replier sets it to 1.3, so that path is protected by accident. `chatReq`
|
|
in the phraser sends no penalty, so `PhraseChat`, `PhraseQuery`,
|
|
`PhraseNudge` and `PhraseReminder` all run at the default 1.0. Filed as
|
|
**531**.
|
|
Two guesses were wrong on the way and are recorded so nobody repeats them.
|
|
It is not reasoning tokens: the probe returned `reasoning_content` of length
|
|
0, and the grammar constrains output from the first token. It is not the
|
|
`--cache-ram 512` limit either: that is MiB of prompt cache and the 512 that
|
|
was hit is a token count.
|
|
The wandering is a second thing and stays on 287.
|
|
|
|
**The wake path cannot be checked here, and that is now the decision rather
|
|
than a gap.** `mavwaked` and `mavenclient` appear in no compose file and run as
|
|
no host process. They are not going to. They belong on a client machine in the
|
|
room he is standing in, because homesrv's microphone is real and in the wrong
|
|
room — **463**, written up in `docs/plans/17-where-the-voice-loop-runs.md`.
|
|
|
|
So the wake word and the VAD gate are covered by their unit tests and by
|
|
nothing else, and no session at this box changes that. Checking them needs a
|
|
machine with a microphone running both binaries against a TCP-listening mavend.
|
|
`ipc.Dial` already speaks `tcp://host:port?token=...`, so the work is not
|
|
protocol work.
|
|
|
|
That machine is workpc (owner's correction, 05-08-2026). This section used to
|
|
call it a machine Maven does not have. That was written when the workstation
|
|
was only a model host. So the remaining work is deploying two daemons and
|
|
asking mavend to listen on TCP. Until that is done, **287** can only be
|
|
half-answered, and step 2 above is push-to-talk, not the voice loop.
|
|
|
|
**319's single-token bug is fixed** (01-08-2026). Single-word Russian utterances no longer come
|
|
back as `не совсем поняла — можешь переформулировать?`. `привет` and `поужинал`
|
|
both pass now: `thinSingleToken` spares social singles and any token carrying a
|
|
verb ending, and only thins a bare nominal like `вода`. A one-word utterance that
|
|
still gets clarified in this session is a new case for the lexicon, not the old bug.
|
|
|
|
---
|
|
|
|
## Session 2: measurement (half a day, mostly waiting)
|
|
|
|
Closes or advances: **320** items 2-4, **278** (make the eval lab routine).
|
|
Also **248** (memory evaluation), **319** (the margin gate) and **323** (the
|
|
startup timeout arm).
|
|
|
|
The resident llama-server cannot be reached by the eval harness. It binds
|
|
`--host 127.0.0.1 --port 0` inside the container, so the port is kernel-assigned
|
|
and never published. Start a second one on a fixed port instead:
|
|
|
|
```sh
|
|
llama-server -m /mnt/hdd1/llms/qwen3/Qwen3-1.7B-UD-Q4_K_XL.gguf \
|
|
--host 127.0.0.1 --port 18100 -c 4096 -ngl 99 --no-webui
|
|
```
|
|
|
|
`-c 4096` matters. The recorded numbers were measured at that context size, and
|
|
a mismatch invalidates the comparison.
|
|
|
|
Then:
|
|
|
|
```sh
|
|
make eval-models MAVEN_LLM_URL=http://127.0.0.1:18100 # want ~72.7% cascade
|
|
make eval-router # classifier baseline
|
|
MAVEN_LLM_URL=http://127.0.0.1:18100 make eval-phrasing # persona checks, slow
|
|
make eval-recall
|
|
```
|
|
|
|
A large miss against 72.7% means the deploy differs from the bench harness.
|
|
|
|
`make eval-router` is also the aggregate ONNX lifecycle gate. When the default
|
|
runtime path is non-empty it requires both `TestONNXBaseline` and
|
|
`TestONNXRoutingHeads` to load their models and execute in the same Go process.
|
|
The final line names both verified gates. A later test that skips because the
|
|
first one consumed the process-global runtime now makes the command fail rather
|
|
than leaving a green package result. To exercise the proof directly:
|
|
|
|
```sh
|
|
MAVEN_ONNX_LIB="$PWD/deps/onnxruntime-linux-x64-1.26.0/lib/libonnxruntime.so" \
|
|
MAVEN_ONNX_REQUIRED_GATES=2 \
|
|
go test -v -count=1 \
|
|
-run '^(TestONNXBaseline|TestONNXRoutingHeads)$' ./internal/router/eval/
|
|
```
|
|
|
|
Set `MAVEN_ONNX_LIB=` explicitly when the intended measurement is the portable
|
|
hash floor; that also disables the aggregate ONNX requirement.
|
|
|
|
**Run on 02-08-2026 @ af9d213. The deploy matches the bench.** `eval-models`
|
|
scored 56 of 77: 72.7% full, 77.9% intent-only, 2 false clarifies and 1 missed.
|
|
That is the recorded figure to the decimal, and calendar sat at 2 of 2, so the
|
|
stage 0 agenda rules hold. `eval-phrasing` scored 21 of 27 on the talk fixture
|
|
against a recorded 20, and the 15 nudge templates passed every check.
|
|
|
|
Two numbers in this repo were wrong, and both flattered the resident model.
|
|
|
|
- **The classifier is not 36.8% and not 31ms.** `make eval-router` reports
|
|
`classifier+onnx: 53/77 (68.8% full)` at p50 16.6µs. The figure repeated here
|
|
and in `CLAUDE.md` predates the stage 0 rules and the seed additions. Both now
|
|
score inside that baseline. The accuracy gap the router buys is
|
|
roughly 4 points, not 36. Re-argue the trade on the real numbers: **464**.
|
|
- **Router latency was measured under contention again.** p50 1.126s, p95 1.58s,
|
|
max 3.24s, against a recorded p50 825ms. The resident model was serving the
|
|
daemon on the same iGPU throughout. Do not record this as a regression, and do
|
|
not record it as a measurement either. Stop the stack before timing the router.
|
|
|
|
`classifier+hash` scores 19.5%, which is the no-ONNX degraded path and is not the
|
|
failure floor the deploy uses. Do not quote it as the classifier baseline.
|
|
|
|
Then three things to decide while the numbers are in front of you:
|
|
|
|
- **319 is done.** 359 gave the LLM path a real confidence signal.
|
|
`thinSingleToken` was narrowed on 01-08-2026, and agenda questions moved to
|
|
stage 0. Missed clarify sits at 1 of 6 and false clarifies at 2. Item 2 point 2
|
|
closed on 02-08-2026: the `make eval-recall` margin sweep is the distribution
|
|
that was asked for, and `0.008` sits at the knee.
|
|
|
|
| delta | answered | false recall |
|
|
|---|---|---|
|
|
| 0.005 | 18/27 | 2/5 |
|
|
| **0.008** | **18/27** | **1/5** |
|
|
| 0.010 | 16/27 | 1/5 |
|
|
|
|
It removes four of five false recalls at no cost in answers, and the next step
|
|
costs two answers for nothing. The hand-picked value survives on evidence.
|
|
- **278's real ask** is making the eval lab routine rather than building it. It
|
|
is built. Decide whether it runs on a timer, on every merge, or on demand, and
|
|
the task can close.
|
|
- **248** is the memory evaluation loop. It ships, it writes notes, and it cannot
|
|
speak. `make eval-recall` covers the retrieval half. The open question is whether
|
|
a written evaluation nobody reads is worth the tick.
|
|
|
|
**323 is down to one check.** PR #90 covered the spawn path and took phraser
|
|
coverage to 76.9%. Only the 60s startup timeout arm is untested, because testing it
|
|
needs a `StartupTimeout` field on `Config` rather than a test-only hack. While you
|
|
are on the box, time a cold 1.7B load off spinning disk. If it runs near 60s, the
|
|
default is too tight and the field earns itself twice.
|
|
|
|
Warm, it is nowhere near. A second llama-server answered `/health` 1.8s after
|
|
launch at `n_ctx 4096` on 02-08-2026. That is page cache, so it does not settle
|
|
the question. A cold read needs a cache drop, which needs root.
|
|
|
|
**`CheckFeminine` has a false positive.** On 02-08-2026 it failed
|
|
`query-notes-do-not-answer` for `ты заплатил`, calling it masculine
|
|
self-reference. Masculine second person is correct, because the owner is male.
|
|
The check matches a masculine
|
|
past-tense verb before `за` without confirming the subject is `я`. Fix it in
|
|
`internal/phraser/eval/checks.go` before trusting a phrasing score to the case.
|
|
The real talk-fixture score on that run is 22 of 27, not 21. Tracked as **462**.
|
|
|
|
Item 4 of **320** needs a permission I do not have. Kill the `llama-server`
|
|
pid under `maven-mavend-1`, post a turn, and confirm it still completes
|
|
through the classifier. Either grant it or run it yourself. It is the only
|
|
check that the failure floor catches a mid-session model death.
|
|
|
|
---
|
|
|
|
## Session 3: the interaction batch (a day, or five sittings)
|
|
|
|
These need real use rather than a command, grouped by what one sitting covers.
|
|
|
|
**Morning and delivery** (**280**, **281**, **128**, **283**, **285**):
|
|
open `/morning`, walk the seven required behaviours, then check the four
|
|
interruption outcomes and the digest gap. The presence half of this sitting
|
|
cannot run. `desk-active.sh` is on workpc, but no systemd user unit enables it,
|
|
so no `desk_active` fact has ever been written. That is **15** and needs you at
|
|
that machine.
|
|
**283** is the event intake envelope every reach shares, so a delivery check
|
|
exercises it whether you name it or not. **285** is not verification: the bridge
|
|
framework works and the remaining ask is more adapters. Decide which reach comes
|
|
next, or park it.
|
|
|
|
Run 02-08-2026. **280 is blocked.** No morning routine is configured (**472**).
|
|
`morning.Item` also has no required-versus-optional field, so behaviour 1 cannot
|
|
hold whatever you configure (**473**). **281's digest gap is closed**, and
|
|
its presence rule passes on inspection. Three of its five items need traffic the
|
|
box has not had. **283 is blocked**: nothing feeds the intake journal. **128
|
|
found the worst defect of the whole session, see below.**
|
|
|
|
Three of 472's five blockers were cleared the same day, in `deploy/mavend.json`
|
|
and `docker-compose.yml`.
|
|
|
|
- A `morning_routines` block, one routine `утро` 08:00-11:00 with medicine,
|
|
water and pets. It is live: the dispatcher logged `dropped morning:утро (sev1,
|
|
presence=away)`, so the plan builds and the nudge is proposed. 280's
|
|
behaviours and 128 step 11 are checkable now. 473 still stands.
|
|
- `-ambient-token` on mavweb, value in a gitignored `/.env` that docker compose
|
|
reads for interpolation. `/api/ambient` answers 401 without the token and 201
|
|
with it, storing `calendar_event_20260802_Standup`. 283 step 5 and 128 step 8
|
|
are unblocked. The token is a flag, so it shows in `ps` inside that container.
|
|
The zenmoney and IMAP secrets are read from files instead. Ingest also
|
|
reads the notification's wall clock as UTC and stores a 14:30 meeting at 18:30
|
|
(**482**).
|
|
- `feeds` (two sources), `crawl.on_demand` and `netscan.enabled`. The intake
|
|
journal now fills: `/events` holds `scan:lan` and `ambient:notif` rows.
|
|
|
|
Two are not mine to clear. No sibling has a `token` in `deploy/mavend.json`, so
|
|
273 steps 6 and 8 need a credential decision. Nexus has no entities and Praxis no
|
|
attention items, so 272 step 3 needs seed data whose content is the owner's call.
|
|
|
|
For **285**, two facts bear on the choice. Synapse is already running on this box
|
|
and healthy, so a Matrix reach has a live target and needs no new service. And
|
|
mavweb is already a PWA with a service worker, which 285 itself calls the highest
|
|
value adapter left. Today's reaches are ntfy, telegram and voice.
|
|
|
|
**Query sources** (**258**, **286**): ask her something the RSS feeds answer and
|
|
something only a ZIM answers, with the search block on. Live search leads and the
|
|
ZIMs are the fallback since 02-08-2026. **286**'s remaining half is doc and
|
|
git ingestion, which is build work, not a check.
|
|
|
|
**Read `/trace` for this.** It carries two tables since 06-08-2026 (V-564). The
|
|
nudge-rule trace it always had, and below it the **turn decisions**: one
|
|
collapsible record per utterance. Each names every claimant, what it would have
|
|
made the turn, the score it reported, and whether it won, declined, lost or was
|
|
**never asked**. That last one answers "did Kiwix pass, or was it never
|
|
reached". The log lines cannot tell you that. The ring holds the last 25 turns
|
|
in daemon memory and is empty after a restart, so read it in the same sitting.
|
|
`/chat` still shows the claiming source as a badge, and the `voice: query
|
|
claimed by source` line is still in `docker compose logs mavend`.
|
|
|
|
Run 02-08-2026, 20 turns. **Search leads and the personal boundary holds.** Every
|
|
world question that reached the boundary was claimed by search. All three
|
|
personal questions produced no search and no kiwix line at all.
|
|
|
|
The rest of this sitting went badly. **Kiwix has zero live coverage.** SearXNG
|
|
returns four results for everything, including two invented nonsense terms. So
|
|
`querySearch` always claims, and Kiwix is unreachable code as deployed. The ZIM
|
|
half of the 02-08-2026 decision is unverified. A ZIM answer cannot signal a
|
|
silent search failure, because a ZIM answer cannot happen.
|
|
**Ordering defects** in feeds and calendar, plus 258 step 1's utterance not
|
|
working: **474**. And the sitting independently found stage 2 of **470**.
|
|
|
|
**Tasks and calendar** (**129**, **130**, **127**, **126**, **246**): capture a
|
|
task by voice, confirm it lands, check prioritisation ordering is not nonsense.
|
|
**246** (mail reader) also exercises the `IngestMail` rung that moved to
|
|
`AuthWrite` this morning.
|
|
|
|
Run 02-08-2026. **129 passes.** The page and the spoken answer agree on ordering.
|
|
The undistinguished task carries no invented reason on either surface, which is
|
|
the thing 129 asks for. **130 fails outright** and **127 half fails**:
|
|
**467**, **469**. **246 cannot be run**: `mavmaild` is commented out in
|
|
`docker-compose.yml` and there is no `email` block, so nothing in steps 4-13 is
|
|
reachable. The `IngestMail` rung does sit at `AuthWrite`
|
|
(`internal/auth/policy.go:96`, asserted in `auth_test.go:421`), verified by
|
|
reading only.
|
|
|
|
**Routines and patterns** (**43**, **46**, **247**, **254**): these need history
|
|
to detect against. If the database is thin after the outage, they may have
|
|
nothing to propose, which is not a failure. Check `/routines` before
|
|
concluding anything.
|
|
|
|
Run 02-08-2026. The answer is the middle case: **the detector ran and found
|
|
nothing.** The tick loop is live, and `detectPatterns` is called unconditionally
|
|
at `cmd/mavend/tick.go:227`. It has run about 25 times since the restart. It
|
|
finds nothing because the events table is empty upstream of it. Rows land there
|
|
only from `pattern.Extract` at fact-write time, and `Extract` requires the fact
|
|
value to match a closed 7-action lexicon. All 200 facts on `/history` are
|
|
`page_heartbeat`, `netdata_alarm`, `quiet_hours`, `name`, `service_down` and
|
|
`рост`. Not one lexicon hit, so no event can exist, let alone the four one pair
|
|
needs. **46 step 5 passes**: `/routines` renders `noticed 0` with the empty state
|
|
and the hint string.
|
|
|
|
Two things block this sitting, and both are build work. The seeding recipe on
|
|
**43** goes through `sqlite3` and cannot work. And `pattern.Detect` has no
|
|
minimum-interval floor, so seeding by hand mints a permanent false routine
|
|
(**468**). Do not try to seed a pattern with four fast chat turns.
|
|
|
|
**Ecosystem** (**272**, **273**, **276**): nexus, hexis and praxis are wired and
|
|
logged clean at boot.
|
|
|
|
Run 02-08-2026, read-only half. All three answer `/health` 200 and `/ecosystem`
|
|
lists 18 Hexis capabilities with correct read-only and mutating badges. **272 and
|
|
273 are blocked on empty data**, not on code. Nexus holds no entities, Praxis
|
|
holds no attention items, and the Calls panel has never recorded a call. See
|
|
**472**, and read its warning first. 273's trace fix has never been validated
|
|
here. An empty Calls panel is exactly what the old bug looked like. The page is
|
|
`/ecosystem`, not `/siblings`.
|
|
|
|
**276 ran 02-08-2026 and the suite is sound.** 17 `TestEcosystem_` cases pass
|
|
under `-race`, not the 10 the task describes. The mutation check bites: patching
|
|
the Nexus-error branch of `handleHexisAct` to `return ""` fails
|
|
`TestEcosystem_MalformedNexusResponseFailsClosed` on the expected line.
|
|
|
|
Steps 4 and 6 could not be checked through chat, because no utterance reaches
|
|
Praxis (**475**). «что требует внимания» routes to `intent=query` and is answered
|
|
by the search leg, identically whether `ecosystem-praxis-1` is up or stopped. The
|
|
degraded string never appears because its branch is never entered. Step 5 is
|
|
blocked the same way: `перезапусти muzick indexer` clarifies on
|
|
`HasFn:false`, and the router had already rewritten the entity name to
|
|
`музик индексер` (**476**).
|
|
|
|
Both steps were checked on `/ecosystem` instead, which reads Praxis directly.
|
|
With Praxis stopped the card reads `praxis — unreachable` while Nexus and Hexis
|
|
keep rendering. On `docker start` the card returns to `nothing needs attention.`
|
|
with no mavend restart. Independent degradation and recovery both hold.
|
|
|
|
**Workstation offload** (**492**): never run, and added to this plan on
|
|
2026-08-04. It covers **485**, which shipped in PR #97. Three states, one rule:
|
|
silent when the workstation would only do the job better, named when the resident
|
|
model cannot do the job at all.
|
|
|
|
1. **Card free.** mavgpud 200, llama-server holding gemma-4-12b. A routing turn
|
|
and a phrased reply both complete through the workstation. Confirm that from
|
|
the mavgpud request log, not from the answer sounding good. Nothing in the
|
|
answer says where it was phrased.
|
|
2. **Card held.** Start a training run so mavgpud yields and answers 503. The same
|
|
turns complete on Qwen3-1.7B with no mention of the fallback. Then kill the
|
|
card mid-utterance, with a request in flight. That is the case no unit test
|
|
reaches and the one most likely to hang.
|
|
3. **Machine asleep.** Suspend workpc. It must be indistinguishable from held.
|
|
Bring it back and confirm the prober re-admits it inside one 15s interval, with
|
|
no mavend restart.
|
|
|
|
Two things are likely wrong. A remote that accepts the connection and then never
|
|
answers is worse than a 503. `timeout` is 90s, so measure what a turn waits. And
|
|
two models mean two prompt renderings: `check_prompt_parity.py` guards Go against
|
|
the relabelling prompt, not gemma against Qwen, so confirm `{"response","mood"}`
|
|
parses from both.
|
|
|
|
**workpc is running training as of 2026-08-04**, so the held state is available
|
|
today and the free state is not. Run step 2 first, out of order.
|
|
|
|
Write down one number at the end. Read the mavgpud journal and record the
|
|
fraction of a working week the card is free. That is what **488** left open, and
|
|
it decides whether the offload is worth carrying.
|
|
|
|
**Operations** (**249**, **250**): both ran 02-08-2026. The code is correct and
|
|
neither lever can be pulled on this box. See **477**.
|
|
|
|
**250** passes steps 1, 2, 3, 9 and 10 on the deploy. The capability announces
|
|
itself. `/models` names the model llama-server reports, not the config filename.
|
|
Asking her to switch models does nothing. Removing `swap_models` renders `swap
|
|
not configured`. Step 4's refusal half passes at HTTP 403, and the 403 comes from
|
|
mavend rather than mavweb. WebAuthn is unconfigured, so the web gate fails open
|
|
and the wire gate fails closed. Steps 5 to 8 need a passkey assertion nothing on
|
|
this box can produce. They pass in test: 13 swap cases and 7 page cases covering
|
|
drain, mid-swap refusal, rollback, failed rollback and the not-owned refusal.
|
|
|
|
**249** passes steps 1 and 2. Step 3 stops it. `mavupdate` health-checks over
|
|
`/run/maven/mavend.sock`, which is `srw------- 1 10001 999` inside a docker
|
|
volume. The host owner cannot traverse `/var/lib/docker/volumes` and cannot
|
|
connect to a socket owned by an in-container uid. `mavupdate` assumes a
|
|
host-installed daemon and the deploy is containers. Do not sudo around this.
|
|
|
|
---
|
|
|
|
## Housekeeping (done 02-08-2026, and this section was mostly wrong)
|
|
|
|
This section claimed eleven tasks were not verification work. **Three were not.
|
|
The other eight are.** Every one of the eight has shipped, tested code behind it.
|
|
The error ran one way: it wrote off work that is ready to check. Do not trust a
|
|
"nothing is built" line in this plan without grepping for the package first.
|
|
|
|
Relabelled to `Blocked:`, claim verified:
|
|
|
|
- **125** zenmoney. `internal/zenmoney/` ships and is tested against a fixture.
|
|
`deploy/zenmoney.token` does not exist and the compose mount is commented out.
|
|
One token unblocks it.
|
|
- **256** Home Assistant. `internal/smarthome/` ships, the `smarthome` block sits
|
|
in `deploy/mavend.json` at `enabled: false`, and 8123 and 1883 are closed.
|
|
- **14** cold-start unlock. The seam is real at `cmd/mavend/main.go:128` and
|
|
`internal/webauthn/prf.go` is in place. `lockedAPI` is gone, replaced by
|
|
`Server.Check` in `internal/ipc/server.go`. Gated on an authenticator that
|
|
implements the WebAuthn PRF extension, which is hardware, not code.
|
|
|
|
Left alone, because the claim here was false:
|
|
|
|
- **284** simulator. `cmd/mavend/simulator_test.go`, three scenarios under
|
|
`cmd/mavend/testdata/scenarios/`, and a `simulate` target at `Makefile:98`.
|
|
**Run 02-08-2026: all three scenarios pass**, plus the determinism and
|
|
backwards-step guards. One defect found, see below.
|
|
- **288** STT golden audio. Four WAVs and `golden_v1.json` are committed under
|
|
`cmd/mavsttd/testdata/`, the make targets exist, and `models/stt/ggml-small.bin`
|
|
is on the box. Session 1 lists 288 as blocked on fixtures, which is wrong.
|
|
**Run 02-08-2026: all four pass**, WER at or under ceiling with no drift.
|
|
|
|
| fixture | transcript | WER | ceiling |
|
|
|---|---|---|---|
|
|
| ru_reminder | `Напомни мне через час позвонить маме.` | 0.00 | 0.10 |
|
|
| ru_fact | `А отметь, что я выпил воды.` | 0.20 | 0.25 |
|
|
| ru_query | `Что у меня сегодня по календарю?` | 0.00 | 0.10 |
|
|
| en_act | `Restart the web server and check the disk space.` | 0.00 | 0.10 |
|
|
|
|
That also settles a session 1 worry indirectly: whisper.cpp works on Vulkan
|
|
after the redeploy. Only the mic and the wake path remain unproven.
|
|
|
|
**The simulator routes with an empty seed set.** Every `make simulate` run logs
|
|
`loaded 0 seed examples from models/seeds`, seven times per scenario. The test
|
|
runs from `cmd/mavend`, and the seed path is relative to the repo root. The
|
|
scenarios still pass, which means they pass without the classifier having any
|
|
seeds to match against. Whatever 284 is proving, it is not proving the routing
|
|
the deploy runs. Fix the path before trusting a green simulator.
|
|
- **257** Bluetooth. The bluez half is genuinely absent. The LAN-scan half shipped
|
|
(`internal/netscan/`), and steps 1-9 run today. Only step 10 is Bluetooth, so
|
|
relabelling the whole task would bury real pending work.
|
|
- **251** MCP, **253** hearing, **259** crawler. All three ship
|
|
(`internal/mcp/`, `internal/capture/`, `internal/crawl/`) with no external gate.
|
|
Fully checkable. `259`'s step 1 wants no `crawl` block in `deploy/mavend.json`,
|
|
and there is none, so it is already set up correctly.
|
|
- **252** vision and **255** speaker recognition. Both ship. Each is blocked only
|
|
on a model download: a vision gguf with mmproj, and a speaker embedding model.
|
|
Neither is present under `/mnt/hdd1`. Their refusal-path steps run today.
|
|
|
|
So the honest split is three blocked on a credential or hardware, two blocked on
|
|
a download, and six ready to check. That is roughly a session of real QA this
|
|
plan had written off as backlog.
|
|
|
|
**All six ran on 02-08-2026.** Every one of them is code-correct and stops at the
|
|
deploy. The pattern repeats often enough to be the headline: the packages pass,
|
|
and the box cannot reach them.
|
|
|
|
**251, MCP.** Steps 1, 2, 3, 4 and 13 pass. Package tests green under `-race`.
|
|
Off-by-default is clean, and the SSRF refusal is exact: without `allow_private`
|
|
the log reads `refusing to connect to a private address: 127.0.0.1` and `/tools`
|
|
shows the server down with zero proposals. Steps 5 to 12 are blocked. `ss -lntp`
|
|
shows the Vikunja MCP server on `127.0.0.1:9100` only, so no container reaches it
|
|
at any address (**478**). `allow_private` does work, measured both ways.
|
|
|
|
**253, hearing.** Steps 1, 2 and 17 pass. `internal/capture` covers 90.3%. Steps
|
|
7 to 16 are blocked on something nobody can work around: no shipped client calls
|
|
`CaptureStart`. There is no `cmd/mavheard`, no mavweb route, and `mavenclient`
|
|
never calls it (**480**). Two of its QA steps are also stale.
|
|
|
|
**Four stale QA steps were rewritten on 04-08-2026** under **480**, against the
|
|
code rather than against what the plans said. All four failed the same way: the
|
|
daemon was right and the step described an older daemon.
|
|
|
|
| Step | Said | Says now |
|
|
|---|---|---|
|
|
| 253/3 | boots with the methods unknown | refuses to boot, `config.go:1651` |
|
|
| 253/10 | no `:transcript` note by default | true only with a summary present |
|
|
| 255/5 | `speaker: enrolment on, recognition BLOCKED` | that line is gone, the capability stays off |
|
|
| 252/3 | `vision: stored image <id-prefix>` | `vision: stored <id>`, `vision.go:199` |
|
|
|
|
Two of them are worth reading past the correction. 253/10 was false in exactly
|
|
the scenario 253/16 creates, because `writeNotes` saves the transcript whenever
|
|
the summary is empty so a dead llama-server does not lose the meeting. And 255/5
|
|
changed because `Recognizes()` was written as the gate, documented as one, and
|
|
never called — calling it turned `enabled` with no model from a half-working
|
|
capability into a refusal. Enrolling into a store nothing can match against is
|
|
not a working half.
|
|
|
|
**257, netscan.** Steps 2, 3 and 9 pass at unit level. Step 1 fails. Steps 4 to 8
|
|
need the block enabled. Step 10 is Bluetooth and stays skipped.
|
|
|
|
**259, crawler.** Steps 1 and 15 pass. Step 2 fails. Steps 3 to 14 need a `crawl`
|
|
block that nobody has written.
|
|
|
|
Both were configured later the same day, and both work. `netscan.enabled: true`
|
|
answers `какие устройства в сети?` with `нашла 3 устройства, из них 2 с вебом, 2 с
|
|
ssh. список записала.` and the scan lands in the intake journal as `scan:lan`.
|
|
`crawl.on_demand: true` answers `посмотри https://lwn.net — что там пишут?` from
|
|
the real page. So **479** is one defect, not the routing defect it was filed as.
|
|
An unconfigured capability declines its own turn instead of naming the gap.
|
|
Nothing is wrong with the routing.
|
|
|
|
257 step 1 and 259 step 2 fail the same way and share a task (**479**). An
|
|
unconfigured capability does not name the gap, so the question escapes to web
|
|
search. `какие устройства в сети?` was answered with a general article about
|
|
network hardware. That is his LAN going to an upstream engine.
|
|
|
|
**252 vision and 255 speaker.** Both confirmed blocked. The disk claim was
|
|
re-verified rather than taken on trust: 16 text-only ggufs under `/mnt/hdd1`, no
|
|
mmproj and no speaker embedding model. Everything not needing the model passes,
|
|
including the two refusals that matter. `TestNewLocalRefusesNonPrivateEndpoints`
|
|
rejects `https://api.openai.com`, and forget really deletes
|
|
(`internal/store/memory.go:145` is a real `DELETE`, not a tombstone). Vision is
|
|
19/19, speaker 22/22, media 16/16.
|
|
|
|
**470 got worse, then closed.** Both poisoned facts showed `voided` on
|
|
`/history` and the defect survived. Re-measured at 15:42, after four restarts:
|
|
`почему небо синее?` still answered `какая последняя версия языка Go?` with no
|
|
`search:` line. What came back was the question he typed, not the value the fact
|
|
held. So the poison was a vector in the memory index, and `revert` did not
|
|
remove it.
|
|
|
|
Repaired in two parts. 470 stopped the writes: a question is never a fact, and a
|
|
void drops the key's vectors. 493 fixed what the index holds. A fact is indexed
|
|
as the fact and not as the utterance, and a correction drops its superseded
|
|
vector too.
|
|
|
|
A poisoned box now repairs itself on the next start. `RepairFactVectors`
|
|
re-embeds every fact vector from the fact it names, and deletes the voided and
|
|
superseded ones. It runs once, guarded by a marker, and logs what it did.
|
|
|
|
---
|
|
|
|
## Needs you specifically
|
|
|
|
Not QA. These are blocked on a decision or a credential only you have.
|
|
|
|
| # | what |
|
|
|---|---|
|
|
| 16 | Create the Kuma API key. `-kuma-key uk5_mavpoll-key` in `docker-compose.yml` is still the placeholder. |
|
|
| 15 | Enable the `desk_active` units on workpc. The script is there; the timer is `not-found`, so the strongest presence signal writes nothing. Blocks the presence half of session 3. |
|
|
| 122 | Finish the CPT run for Qwen3-1.7B. The persona fix depends on it. |
|
|
| 355 | Deploy the Hexis auth change. Was blocked on Maven being under construction, which it no longer is. The client half is vendored and wired. |
|
|
| 357 | Decide whether entity-existence validation is the permanent target guard or whether blessing lands in Nexus. |
|
|
| 275 | Hexis native API and MCP parity. |
|
|
| — | Decide on `-require-stepup`. Making it the default needs WebAuthn configured first, or it locks you out of your own admin surfaces. |
|
|
|
|
317 and 354 closed on 01-08-2026. The step-up gate now covers `POST /api/chat` and
|
|
`/routines`, and the nginx template is locked down with a `maven.<domain>` block for
|
|
mavweb. The `-require-stepup` default is still your call.
|
|
|
|
---
|
|
|
|
## Not this repo
|
|
|
|
Two open tasks sit on the Maven board and are not Maven work. Move them or note
|
|
where they land, so the board stops reading as 50 things Maven owes.
|
|
|
|
- **358** replace the rowid execution cursor with a real seq column. This is Hexis,
|
|
and it must land before any execution retention or pruning does.
|
|
- **362** mirror the router prompt reorder into the relabelling prompt. This is the
|
|
training workspace, enforced by `llm/check_prompt_parity.py` there, not here.
|
|
|
|
---
|
|
|
|
## Suggested order
|
|
|
|
1. Session 1. If the voice loop is broken, nothing else matters.
|
|
2. The `-require-stepup` and Kuma decisions. Five minutes, and it unblocks **16**.
|
|
3. Session 2. **Run on 02-08-2026.** The numbers came back worse for the router
|
|
than the docs claimed. The classifier is 68.8%, not 36.8%, and 16.6µs, not
|
|
31ms. The router buys about 4 points of accuracy for four orders of magnitude
|
|
of latency. Whether that still earns its place is now an open question.
|
|
4. Housekeeping. Cheap, and it makes the remaining backlog honest.
|
|
5. Session 3, split whichever way suits you. Five of its six sittings ran on
|
|
02-08-2026. Read the per-sitting notes before repeating any of them.
|
|
6. The workstation offload sitting (**492**), which has never run. It is last
|
|
because it is newest, not because it matters least. It is the one sitting whose
|
|
subject changes state on its own.
|
|
|
|
The next thing to fix is not in this plan. Four defects say the same sentence:
|
|
a capability is built and no utterance reaches it. **466** (a clarify is global),
|
|
**467** (capture is act-routed), **475** (attention is act-routed), **476** (the
|
|
router rewrites entity names). Routing is where the work is.
|