Files
Maven/CLAUDE.md
T
kami 0ca5748699 Stop the docs claiming the LLM router is off
CLAUDE.md's routing section said "llmrouter is wired nil" and called the
classifier cascade the committed default. That stopped being true when the
integration merge landed: voice.go:214 wires pickLLMRouter, DefaultLLMRouter is
on, and deploy/mavend.json sets llm_router true. It is the first thing anyone
reads before touching the router, so it was pointing the next reader at a
wiring job that is already done.

Rewritten to say the LLM router is the default, the classifier is the failure
floor and must not be deleted, and what the two actually measure — 36.8% at
p50 31ms against 67.5%/72.7% at p50 ~2.7s, a trade accepted on purpose. Names
the one thing still open on that path: Confidence is hardcoded 1.0 in
llmrouter.go, so the LLM never asks for clarification (#359).

Also in CLAUDE.md: the persona line pointed at a memory file that does not
exist, so the actual rule was nowhere in the repo. Written out instead —
feminine self-reference, informal singular address, pet names forbidden but his
name allowed — plus the three eval checks that enforce it.

MODEL-BAKEOFF: three claims had gone stale within hours of being written. There
IS a make eval-models target now; the routing numbers ARE the production path,
not a bench artifact waiting on a wiring change; and the truncated 293 MB gguf
is deleted. Struck through rather than removed, since the caveats are part of
how the evening read at the time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 22:37:15 +04:00

7.9 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Maven is a self-hosted, privacy-first voice assistant (Russian + English). Go daemons talking over unix sockets; one resident small model for routing + phrasing; whisper.cpp STT, piper TTS. Deploy target is a Ryzen laptop (homesrv) with Vulkan offload to the Vega iGPU (n_gpu_layers: 99, compose passes /dev/dri + the render gid) — the resident model stays ≤1.7B either way.

Resident model: currently Qwen3-1.7B (UD-Q4_K_XL), stock — not yet the CPT'd one. It replaced Qwen3.5-0.8B on 2026-07-31 because it measured better on both fixtures we have: 67.5% vs 59.7% intent-only on the 77-case RU routing fixture, and 20/27 vs 11-17/27 on the talk fixture. See MODEL-BAKEOFF-31-07-2026.md. It is a Thinking variant, so n_ctx is 4096 — reasoning tokens need the room, and 4096 is what the scores above were measured at.

The target is still the locally CPT'd Qwen3-1.7B (Vikunja #122, training in flight). Stock already speaks good Russian; what it gets wrong is the persona — it writes я рад, masculine, where Maven needs рада. That is what the CPT is for.

Do not bother with sub-500M models. LFM2.5-230M and 350M were measured on 2026-07-31 and both are unusable in Russian: the 350M routes at 5.2% (worse than guessing) and answers "столица Франции?" with the invented non-word "Сторзит"; the 230M replies to Russian in Spanish. Their strong published IFEval/BFCL numbers are English-only. Model files live in /mnt/hdd1/llms, bind-mounted to /opt/maven/models/llm — which shadows the repo's models/llm/, so the LFM2.5 gguf sitting there is not loaded by anything. Swapping the resident model is a one-line change to phraser.model_path in deploy/mavend.json.

See REARCH.md for the target architecture, DESIGN.md for the folded design spec, and AGENTS.md for local-preview + model-download recipes.

Build & test

CGO daemons (mavend, mavsttd, mavttsd, mavenclient) need the vendored toolchain and libs wired through the Makefile — do not call go build on them bare, use make:

make build          # all 8 binaries
make build-web      # single daemon (pure-Go ones: web/waked/poll/caldav build without CGO)
make test           # go test -race across ./internal/... ./cmd/... with CGO env set

Run a single test (must carry the CGO env for packages that touch STT/TTS/voice):

CGO_CFLAGS="-I$(pwd)/deps/include -I$(pwd)/deps/whisper.cpp/ggml/include" \
CGO_LDFLAGS="-L$(pwd)/deps/lib -Wl,-rpath,$(pwd)/deps/lib" \
LD_LIBRARY_PATH="$(pwd)/deps/lib" \
deps/go/go/bin/go test -run TestName ./internal/router/

Pure-Go packages (router, memory, mavweb, …) run under a plain go test ./pkg/.

The daemons (cmd/)

Binary Role
mavend Core. Router, phraser, memory, reminders, digestion tick. Owns the DB + IPC socket.
mavweb HTTP UI + PWA (/dash, /history, /trace, /notifications, /tools); WebAuthn auth. Connects to mavend's socket.
mavsttd Speech-to-text (whisper.cpp, CGO).
mavttsd Text-to-speech (piper subprocess).
mavwaked Wake-word / VAD gate.
mavenclient Voice loop client (mic → stt → core → tts).
mavpoll Telegram long-poll reach.
mavcaldav CalDAV calendar sync.

Daemons are wired socket-to-socket, not linked. internal/ipc is the client/server wire protocol; the config in deploy/mavend.json (with ${VAR} env expansion from gitignored deploy/telegram.env) sets socket paths, model paths, and the phraser/embedder blocks.

Routing — read this before touching the router

internal/router/ has TWO layered engines. The LLM router is now the default and it is on in deploy — this section used to say it was wired nil, which stopped being true on 2026-07-31.

  • LLM router (the intended design, REARCH.md): the resident Qwen3-1.7B (llmrouter.go) emits GBNF-constrained structured JSON, and the SAME model phrases replies. Embedder is demoted from a routing gate to a RAG hint. Wired at voice.go:214 via pickLLMRouter(cfg.Voice.UseLLMRouter(), llmClient); the flag is voice.llm_router (config.go), DefaultLLMRouter is on, and deploy/mavend.json sets it true.
  • Classifier cascade (the failure floor, not dead code): classifier.go + embedder.go nearest-neighbour over frozen seed phrases. It runs when the LLM router is off, when there is no llama-server to talk to (pickLLMRouter logs that and degrades), and on any per-turn LLM error. Do not delete it — routing by seed similarity is the known cause of weak RU query handling, but a turn must never break on the model.

Cascade order: stage0.go exact-match fast-path → LLM router (when non-nil) → classifier fallback. Any LLM error falls through to the classifier so a turn never breaks on the model.

Measured on the 77-case RU fixture (MODEL-BAKEOFF-31-07-2026.md): the classifier scores 36.8% full accuracy at p50 31ms; Qwen3-1.7B scores 67.5% intent-only / 72.7% through the cascade at p50 ≈2.7s. Accuracy roughly doubled, latency is ~90× worse, and that trade was accepted deliberately. Still open: Confidence: 1.0 is hardcoded in llmrouter.go, so the LLM path never asks for clarification (6/6 refusal cases missed) — Vikunja #359.

LLM output contract

All phrasing paths emit {"response":"...","mood":"..."} (parsed in replier_llm.go and internal/phraser/llmphraser.go), with fallback to plain text and the legacy {"body","summary"}. Mood is a fixed enum. Router prompt is a separate contract: [{"intent":<enum>, key?, value?, text?, verb?}, ...], 7 intents (fact, reminder, note, query, act, chat, system). llm/check_prompt_parity.py in the training workspace enforces that the Go and relabelling prompts remain identical.

Non-goals (hard constraints)

Not a nag, not autonomous. Maven's persona is feminine — Russian self-reference must use feminine forms — рада, not рад; поняла, not понял. The owner is male and is addressed informally: "ты", singular, never "вы"/"ваш" and never "он"/"его" (she talks TO him, not about him). Pet names ("милый", "дорогой") are forbidden; his name ("Ками") is not. The eval enforces this: CheckAddress, CheckFeminine and CheckCringe in internal/phraser/eval/checks.go, scored by make eval-phrasing.

"Never phones home" is DEPRECATED (owner's call, 2026-07-31). It used to be a hard constraint and it is not one any more: a 0.8B — and a 1.7B — does not know enough to answer world questions, so she needs to read external sources. What replaces it:

  • No telemetry, no cloud model, no third-party account. That part never changes. Nothing about Maven is reported to anyone, and inference stays on the box.
  • Local sources first. Kiwix ZIMs on homesrv (Wikipedia, ifixit) before anything on the network. Reading beats recalling for a small model, and a local read costs nothing.
  • External search is allowed and off unless configured, like the weather and telegram capabilities.
  • His notes and facts are never search input. Looking up why the sky is blue and sending his stored personal notes to an upstream engine are different acts. Only the utterance goes out, never the persona block, history, or matched notes.

Web UI conventions

Server-rendered pages share cmd/mavweb/static/ui.css (served at /ui.css) and the nav partial (navHTML in cmd/mavweb/main.go, {{template "nav" "<active-page>"}}). No per-page <style> beyond true one-offs. Wrap every table in <div class=scroll> so wide data pans on a phone. Local preview + headless screenshot recipe is in AGENTS.md.

Vikunja

This repo is project Maven (ID 2) in Vikunja. MCP: http://localhost:9100/mcp (or http://192.168.1.104:9100/mcp from workpc). Feature/bug/deploy tasks go there.