76a6a007ef
The most load-bearing decision in the project was stated four incompatible ways: the docs said Qwen3-1.7B, deploy/mavend.json said Qwen3.5-2B, the repo's models/llm/ held an LFM2.5-1.2B gguf, and five code comments still said LFM. Answering "which model is deployed" meant re-deriving it from scratch every time. Two facts the review missed, found while resolving it: - /mnt/hdd1/llms is bind-mounted over /opt/maven/models/llm, which shadows the repo's models/llm/. The LFM2.5 gguf sitting there was never loaded by anything, so it was not evidence of the deployed model at all. - That library holds Qwen3.5-0.8B, -2B and -4B, and no Qwen3-1.7B. The config pointed at a file that does exist; the docs' Qwen3-1.7B was the stale claim, the reverse of the assumed direction. Qwen3-1.7B is the CPT target, and that training is still in flight (Vikunja #122), so no such gguf exists yet. phraser.model_path moves to Qwen3.5-0.8B (Q4_K_M) — the smallest checkpoint on disk, chosen for latency, and relevant to whether the LLM router is affordable on this box. Docs and comments now say the same thing in one voice: 0.8B resident now, CPT'd Qwen3-1.7B as the target, and the bind-mount shadowing written down so the next reader does not mistake models/llm/ for ground truth. Comments name the model, never a filename, so a swap stays a one-line config change. n_gpu_layers: 99 is correct and stays — compose passes /dev/dri and the render gid for Vulkan offload to the Vega iGPU. CLAUDE.md's "CPU-only" was the stale half of that contradiction and is corrected. phraser.go also dropped a wrong "sub-1b, prompted not trained" size claim: the target is trained end-to-end (RU CPT + joint persona/router SFT). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
99 lines
4.9 KiB
Markdown
99 lines
4.9 KiB
Markdown
# 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.5-0.8B** (`Q4_K_M`), the smallest checkpoint in the gguf
|
|
library, picked for CPU/iGPU latency. The **target** is the locally CPT'd **Qwen3-1.7B**; that
|
|
training is still in flight (Vikunja #122), so no such gguf exists yet. 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`:
|
|
|
|
```sh
|
|
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):
|
|
|
|
```sh
|
|
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 and the committed default is an **interim
|
|
stopgap, not the intended design** (see memory `routing-architecture-target`):
|
|
|
|
- **Target (REARCH.md):** LLM-as-router. One 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.
|
|
- **Current stopgap:** `llmrouter` is wired `nil` (around `voice.go`), so the
|
|
`classifier.go` + `embedder.go` nearest-neighbour cascade actually runs. It routes by
|
|
similarity to frozen seed phrases — the known cause of weak RU query handling.
|
|
|
|
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.
|
|
|
|
## 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)
|
|
|
|
Never phones home. Not a nag, not autonomous. Maven's persona is **feminine** — Russian
|
|
self-reference must use feminine forms (the user is male; see memory `maven-persona-gender`).
|
|
|
|
## 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.
|