Files
Maven/AGENTS.md
T

182 lines
7.1 KiB
Markdown

# 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:
```sh
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:
```sh
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):
```sh
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:
```sh
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`):
```sh
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`**:
```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 deployed resident model is stock **Qwen3-1.7B** (`UD-Q4_K_XL`), a Thinking
variant at `n_ctx` 4096. `CLAUDE.md` carries the rule on which models qualify.
Without a configured model, `StubPhraser` plus the classifier remain the
deterministic floor.
A locally trained Qwen3-1.7B checkpoint is still in flight (V-122). Training
runs Qwen3 Base, then RU CPT, then joint persona and router SFT, then a merged
GGUF. The
runbook is `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 point `model_path` at it.
**Configure in `deploy/mavend.json`.** This is the deployed `phraser` block:
```json
"phraser": {
"model_path": "/opt/maven/models/llm/qwen3/Qwen3-1.7B-UD-Q4_K_XL.gguf",
"bin_path": "llama-server",
"n_gpu_layers": 99,
"n_ctx": 4096,
"cache_ram_mib": 512,
"timeout": "60s"
}
```
The daemon spawns `llama-server` as a subprocess. The router and replier reach
that one server through the shared `internal/llm` client. Model files live in
`/mnt/hdd1/llms`, bind-mounted over `models/llm/`, so a gguf sitting in the repo
is loaded by nothing.
Telegram tokens are read from `deploy/telegram.env` (gitignored), expanded
via `${VAR}` in the JSON config.
The cascade order, and which stage may decline to the next, is in
`docs/routing.md`. It is not restated here.
## 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.