Files
Maven/AGENTS.md
T
claude a99932b427 Hear speech instead of loudness in mavwaked (V-487)
silero-vad replaces the energy threshold when -vad-model points at it.
Everything after the speech decision is the same state machine: the speech
hold, the silence hold, the length cap and the utterance buffer.

The model window is 512 samples and the capture frame is 480, so silero.go
re-chunks across frames. main.go claimed the two matched, which was true of
silero v4.

Stage two, the wake word, is not here. It needs a Russian keyword model that
does not exist yet.
2026-08-09 01:43:32 +04:00

7.1 KiB

Maven — Agent Context

Vikunja

This repo maps to Maven (project ID: 2) in Vikunja. Feature work, bugs, deployment tasks all go here. MCP endpoint: http://localhost:9100/mcp (or http://192.168.1.104:9100/mcp from workpc)

The sibling services (Nexus, Praxis, Hexis)

Maven is the conversational front end of a four-service ecosystem. The other three live in sibling repos next to this one.

Service Repo Port Answers
Nexus ../nexus 9740 who or what is this name
Praxis ../praxis 8989 what needs attention
Hexis ../hexis 9741 what can be run, and running it

Division of labour: Nexus identifies, Praxis observes, Hexis acts, Maven understands and coordinates. Maven is not the source of truth for any of the three. The full contract is docs/ecosystem.md, and the constraints that bite during implementation are summarised in CLAUDE.md.

Where things are in this repo:

  • cmd/mavend/ecosystem.go holds nexusClient and praxisClient. The Hexis client is vendored from github.com/kami/hexis/pkg/client.
  • cmd/mavend/ecosystem_acts.go routes an act through capability discovery.
  • cmd/mavend/factenrichment.go resolves each stored fact's Subject against Nexus on a background poll loop, with backoff and no give-up.
  • internal/store/entityfacts.go holds the entity-tagged fact rows.
  • Config blocks are nexus, praxis and hexis in deploy/mavend.json. Each is optional. Absent means that integration is dark, not broken.

Bring the whole ecosystem up locally:

docker compose -f deploy/ecosystem/docker-compose.yml up -d

That builds all three from the sibling working trees, so commit or stash there first. Each publishes on loopback at the port above. Maven reaches them by service name on the shared compose network.

Testing without them running: cmd/mavend/fakeecosystem_test.go provides stubs, and cmd/mavend/ecosystem_degraded_test.go covers each service being unreachable.

Rendering / previewing the web UI locally

To see mavweb pages with real data without touching the production stack:

R=/tmp/mvn-preview; mkdir -p $R
go build -o $R/mavend ./cmd/mavend/ && go build -o $R/mavweb ./cmd/mavweb/
cat > $R/mavend.json <<EOF
{ "db_path": "$R/maven.db", "socket_path": "$R/mavend.sock",
  "state_dir": "$R", "tick_interval": "10s" }
EOF
$R/mavend -config $R/mavend.json &
$R/mavweb -addr 127.0.0.1:9299 -core $R/mavend.sock &
  • No models/voice/phraser config needed — the phraser stub covers it; mavend runs fine bare. mavweb serves /, /dash, /history, /trace, /notifications, /tools.
  • Socket path must be short — unix sockets cap at ~108 chars; a deep tmp dir fails with bind: invalid argument.
  • Seed data through ipc.Client (internal package — the seeder must live inside the module, e.g. a throwaway cmd/seedtmp/main.go, deleted after): WriteFact, RecordNudge+ResolveNudge, CreateReminder, ProposeTool.
  • /trace is empty until the first tick fires (wait one tick_interval).
  • Screenshots: chromium --headless --disable-gpu --screenshot=out.png --window-size=1280,900 --hide-scrollbars --virtual-time-budget=2000 http://127.0.0.1:9299/dash (use --window-size=430,900 for the phone/PWA view). Always pass --virtual-time-budget — without it the screenshot can snap mid-layout and silently drop elements (the PWA lang toggle "disappeared" this way).

Embedder model for intent routing

The router uses a multilingual sentence embedder to classify intents and recall notes. Without it, the floor HashEmbedder is used — deterministic but weak (Russian recall rarely clears the confidence gate, many commands fall to "clarify").

Download the embedder (ONNX, ~120 MB):

make download-embedder

This fetches multilingual-e5-small (384-dim, 12-layer, Russian and English) to models/embedder/multilingual-e5-small/. It is an asymmetric retrieval model: the code puts query: in front of a question and passage: in front of a stored note, which is how e5 was trained. The quantized file is the one that is downloaded, deployed and measured.

Voice activity model for mavwaked

mavwaked decides an utterance has started with silero-vad when -vad-model points at it, and with an energy threshold when it does not. The model is 2.3MB and is not committed:

mkdir -p models/vad
curl -sL -o models/vad/silero_vad.onnx \
  https://github.com/snakers4/silero-vad/raw/master/src/silero_vad/data/silero_vad.onnx

It needs the same libonnxruntime.so the embedder needs, passed as -onnx-lib or read from MAVEN_ONNX_LIB. The measurement is docs/evals/2026-08-09-silero-vad.md, and the tests skip without the file.

Also need ONNX Runtime (libonnxruntime.so):

curl -sL "https://github.com/microsoft/onnxruntime/releases/download/v1.15.1/onnxruntime-linux-x64-1.15.1.tgz" | tar xz
sudo cp onnxruntime-linux-x64-1.15.1/lib/libonnxruntime.so* /usr/local/lib/

Configure in deploy/mavend.json:

"voice": {
  "embedder": {
    "model_path": "models/embedder/multilingual-e5-small/model_quantized.onnx",
    "tokenizer_path": "models/embedder/multilingual-e5-small/tokenizer.json",
    "lib_path": "/usr/local/lib/libonnxruntime.so"
  }
}

Without the embedder block, the daemon uses HashEmbedder (works, but weak on Russian recall — you may see many "clarify" responses).

Qwen3 resident model for router + phraser

The target daemon uses the locally trained Qwen3-1.7B checkpoint for both routing and phrasing. Training is Qwen3 Base → RU CPT → joint persona/router SFT → merged GGUF, and is still in flight (#122) — until it lands, the deployed resident model is stock Qwen3.5-0.8B (Q4_K_M), see deploy/mavend.json. Without a configured model, StubPhraser plus the classifier remain the deterministic floor.

During training, use the runbook in docs/plans/2026-07-18-qwen3-resident-training-eval.md. After the decision gate and SFT pass, copy the merged GGUF into the mounted model directory and set:

"phraser": {
  "model_path": "/opt/maven/models/llm/Qwen3-Maven-1.7B-Q8_0.gguf",
  "bin_path": "llama-server",
  "n_gpu_layers": 99,
  "n_ctx": 2048
}

Configure in deploy/mavend.json — the phraser block points at this model and the daemon spawns llama-server as a subprocess. The router and replier use the same llama-server via the shared internal/llm client.

Telegram tokens are read from deploy/telegram.env (gitignored), expanded via ${VAR} in the JSON config.

Routing is Qwen-first with classifier fallback. The LLM router runs after stage-0 (exact-match grammar) and before the classifier cascade. On any error or parse failure, the classifier handles the utterance — the turn never breaks on the model.

Web UI conventions

  • All server-rendered pages share cmd/mavweb/static/ui.css (served at /ui.css) and the nav template partial (navHTML in cmd/mavweb/main.go, invoked as {{template "nav" "<active-page>"}}). New pages must link both — no per-page inline <style> beyond true one-offs.
  • Wrap every table in <div class=scroll> so wide data pans on a phone instead of breaking the layout.