diff --git a/CLAUDE.md b/CLAUDE.md index 6c0215a..78b0890 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,17 +2,21 @@ Guidance for Claude Code (claude.ai/code) working in this repository. -This file is loaded into every session, so it carries rules and not history. A -measurement lives in `docs/evals/-.md` and is never edited after the -day. A subsystem's reasoning lives in a living doc under `docs/`. When a line -here says "see X", read X before changing that subsystem. +**This is a rules file.** It loads into every session, so it carries only what +changes what an agent does. A measurement belongs in `docs/evals/`, dated and +never edited after the day. A subsystem's reasoning belongs in its living doc +under `docs/`. Read that doc before changing the subsystem. | Read this | Before | |---|---| | `docs/routing.md` | touching `internal/router/` or `queryWalk` | +| `docs/deployment.md` | touching a daemon, compose, a systemd unit or the web UI | | `docs/offload.md` | touching a daemon seam or adding a model caller | +| `docs/world.md` | touching search, Kiwix or the world chain | +| `docs/language.md` | changing a prompt contract or a Russian word list | | `docs/ecosystem.md` | touching Nexus, Praxis or Hexis | | `docs/rearchitecture.md`, `docs/design.md` | changing the shape of anything | +| `docs/workflow.md` | the five stores, the doc tiers, the guards | | `AGENTS.md` | local preview, screenshots, model downloads | ## What Maven is @@ -21,470 +25,176 @@ A self-hosted, privacy-first voice assistant in Russian and English. Go daemons talk over unix sockets. One resident small model routes and phrases. whisper.cpp does speech-to-text and piper does text-to-speech. -The deploy target is a Ryzen laptop (homesrv) with Vulkan offload to the Vega -iGPU (`n_gpu_layers: 99`, compose passes `/dev/dri` and the render gid). The -resident model stays at 1.7B or under either way. +The resident model is **Qwen3-1.7B** (`UD-Q4_K_XL`) on homesrv, a Thinking +variant at `n_ctx` 4096. Keep it at 1.7B or under. Sub-500M models are unusable +in Russian (`docs/evals/2026-07-31-model-bakeoff.md`). Model files live in +`/mnt/hdd1/llms`, bind-mounted over the repo's `models/llm/`, so a gguf sitting +in the repo is loaded by nothing. -### The resident model +The workstation is workpc and it holds the remote model and speech-to-text. +**It is never assumed up.** **Fall back silently** when it would only do the job +better. **Name the gap** when the resident model cannot do the job at all. -**Qwen3-1.7B** (`UD-Q4_K_XL`), stock, not yet the CPT'd one. It is a Thinking -variant, so `n_ctx` is 4096. Reasoning tokens need the room, and 4096 is what -every score was measured at. - -The target is the locally CPT'd Qwen3-1.7B (V-122, training in flight). Stock -already speaks good Russian. What it gets wrong is the persona. It writes `я рад` -where Maven needs `рада`. - -**Do not bother with sub-500M models.** LFM2.5-230M and 350M were measured on -2026-07-31 and both are unusable in Russian -(`docs/evals/2026-07-31-model-bakeoff.md`). Their published IFEval and BFCL -numbers are English-only. - -Model files live in `/mnt/hdd1/llms`, bind-mounted to `/opt/maven/models/llm`. -That **shadows** the repo's `models/llm/`, so a 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`. - -### The workstation - -Model work moved to workpc on 2026-08-02 (owner's call). homesrv cannot grow a -GPU and workpc has 16GB of VRAM. So the resident model, speech-to-text and -text-to-speech are preferred remotes with a floor on homesrv. - -Three rules: - -- **The workstation is never assumed up.** -- **Fall back silently** when it would only do the job better. -- **Name the gap** when the resident model cannot do the job at all. A world - question goes through `LLMPhraser.PhraseWorld` and returns `worldGap` - (`cmd/mavend/worldmodel.go`) rather than an invented answer. A box with no - `workstation` block behaves exactly as it did before the seam. - -Routing and replies prefer the workstation through `modelSeam`. Nudge and -reminder phrasing prefer it inside the phraser. `docs/offload.md` says which -caller is which. - -**The embedder stays on homesrv permanently**, because it backs that floor. It is -multilingual-e5-small, quantized and asymmetric. `EmbedQuery` and `EmbedPassage` -apply the `query:` and `passage:` prefixes it was trained with. Calling plain -`Embed` on a note is a bug. See `docs/evals/2026-08-04-recall-e5-small.md`. - -### Speech-to-text - -`sttSeam` in `cmd/mavend/voicewire.go` builds an `stt.Pair` beside `modelSeam`. -It prefers CrisperWhisper 2.0 turbo on workpc with mavsttd as the floor. It takes -only the silent half of the rule, because a worse transcript is still a turn. So -`stt.Pair` has no `TranscribeRemote` and the fallback is never spoken. - -CW2 turbo scores 10.4% WER in Russian against 27.5% for the `ggml-small.bin` -mavsttd loads, over 200 Golos clips -(`docs/evals/2026-08-09-crisperwhisper2-russian-wer.md`). - -**whisper.cpp cannot load CW2 at all.** It reads its language count off the -vocabulary size. CW2's 51897 tokens shift seven special token ids. So CW2 is its -own transformers service on port 8081 (`deploy/cw2/serve.py`). -`stt.HTTPTranscriber` posts raw PCM to it with a bearer token, because audio is -the most sensitive thing that crosses this seam. The switch is `workstation.stt` -in `deploy/mavend.json`, and deleting the block sends every utterance to mavsttd. - -**mavgpud runs that service as a second child.** This is not an optimisation. -CW2 is a ROCm process on the same card, so it registers on the KFD like any -contender. Under its own systemd unit it made mavgpud evict llama-server every -few seconds. That took the model arm down for eight minutes on 2026-08-09. The -card needs one owner. **Any GPU service added beside mavgpud goes in -`cmd/mavgpud`, never in systemd.** CW2 is on the yield clock and not the idle -one. At 1.6GB it denies the card to nobody. - -Text-to-speech has not moved. piper on homesrv is the only synthesizer. +**The embedder stays on homesrv permanently**, because it backs that floor. +`EmbedQuery` and `EmbedPassage` apply the `query:` and `passage:` prefixes +multilingual-e5-small was trained with. Calling plain `Embed` on a note is a bug. ## Build and 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`.** +toolchain wired through the Makefile. **Do not call `go build` on them bare**, +and **do not hand-write the CGO preamble**. This box runs zsh, so an unquoted +`-run Test*` dies on "no matches found" before `go` is reached. `make t` also +carries `-count=1` and sets `MAVEN_ONNX_LIB`. Without that variable the four +`TestONNX*` measurements self-skip and the run still prints `ok`. ```sh -make build # all 11 binaries -make build-web # one daemon (web/waked/poll/caldav build without CGO) -make test # go test -race across ./internal/... ./cmd/... with CGO env set -make t PKG=./internal/router/ -make t PKG=./cmd/mavend/ RUN=TestSimulator +make build # all 11 binaries. make build-web for one (web/waked/poll/caldav skip CGO) +make test # go test -race across ./internal/... ./cmd/... with CGO env set make t PKG=./internal/router/eval/ RUN='TestONNX' V=1 # V=1 for -v, RACE=0 to drop -race ``` -**Do not hand-write the CGO preamble.** Past sessions pasted it about 390 times, -and that is where the shell-quoting failures came from. This box runs zsh, so an -unquoted `-run Test*` dies on "no matches found" before `go` is ever reached. - -`make t` carries `-race`, so a green `make t` cannot turn red under `make test`. -It carries `-count=1`, so a cached PASS from before your edit is never mistaken -for a result. It sets `MAVEN_ONNX_LIB`, which the hand-written recipe did not. -The four `TestONNX*` measurements self-skip when that variable is unset and the -run still prints `ok`. So every targeted eval done the old way reported the hash -ratchet while reading as a real embedder score. - -## The daemons (`cmd/`) - -| Binary | Role | -|---|---| -| `mavend` | **Core.** Router, phraser, memory, reminders, digestion tick. Owns the DB and IPC socket. | -| `mavweb` | HTTP UI and PWA (`/dash`, `/history`, `/trace`, `/notifications`, `/tools`), WebAuthn auth. | -| `mavsttd` | Speech-to-text (whisper.cpp, CGO). | -| `mavttsd` | Text-to-speech (piper subprocess). | -| `mavwaked` | Wake-word and VAD gate. **Not deployed anywhere yet.** | -| `mavenclient` | Voice loop client (mic, stt, core, tts). **Not deployed anywhere yet.** | -| `mavpoll` | Environment poller: netdata alarms, uptime-kuma, zenmoney, wireguard presence. Writes facts, sends nothing. Telegram is `internal/delivery/telegramsink`. | -| `mavcaldav` | CalDAV calendar sync. | -| `mavmaild` | Mail reader (IMAP, read-only). Holds the IMAP password, core never sees it. | -| `mavgpud` | GPU supervisor. **Runs on workpc**, own unit `deploy/mavgpud.service`. Keeps llama-server loaded while the card is free (V-488). Maven never asks it for anything and reads `/health` through `llm.Pair`. | -| `mavupdate` | Not a daemon. Operator CLI a human runs on the box to deploy a new build. | - -Two binaries have no Makefile target and neither is deployed. `mavseal` encrypts -a live tmpfs working copy back to the ciphertext file when mavend was killed -before `defer st.Close()` sealed it. `labelgen` runs the stage 0 grammars over -utterances and prints JSONL, the training data for the routing heads. - -Daemons are wired socket-to-socket, not linked. `internal/ipc` is the wire -protocol. `deploy/mavend.json` sets socket paths, model paths and the phraser and -embedder blocks, with `${VAR}` expansion from gitignored `deploy/telegram.env`. - -### Who is in compose, and who is not +## The daemons +Eleven binaries under `cmd/`, wired socket-to-socket over `internal/ipc`, not +linked. `mavend` is the core and owns the DB and the IPC socket. +`deploy/mavend.json` sets sockets, model paths and the phraser and embedder +blocks, with `${VAR}` expansion from gitignored `deploy/telegram.env`. **`docker-compose.yml` runs five**: `mavend`, `mavsttd`, `mavttsd`, `mavweb`, -`mavpoll`. Count against compose, not against the table above. Four daemons are -absent and each absence has a different reason. +`mavpoll`. Count against compose, not against `make build`. `mavwaked` runs on +workpc under systemd. `docs/deployment.md` says who else is absent and why. -`mavmaild` and `mavcaldav` are commented out, each with the reason beside it. The -first needs a mail account and the second a CalDAV account, and this box has -neither. Two things ride on the CalDAV absence (V-644). Agenda questions route to -`IntentQuery` at stage 0, and the `calendar` query source then reads a table -nobody writes. And `loop.State.CalendarBusy` is fed by the same facts, so the -gate's "do not nag mid-meeting" is permanently false. - -`mavwaked` and `mavenclient` are absent **by decision** (V-463, -`docs/plans/17-where-the-voice-loop-runs.md`). homesrv has a microphone, because -it is a laptop, but it is in the wrong room. They belong on a client machine -where the owner is standing, and that machine is workpc. `ipc.Dial` already takes -`tcp://host:port?token=...`, so V-515 is a deployment and not a build. - -Until then the wake word and the VAD gate are covered by unit tests and nothing -else. Push-to-talk through `/dash` is what QA covers. Deploying them does not by -itself prove a wake word. `mavwaked` has no keyword model (V-487 stage two), so -the loop runs open until that lands. - -**Passwords are read from files, never taken as flag values.** `mavcaldav` uses -`-pass-file` and `-render-pass-file`. `mavpoll` and `mavmaild` follow the same -rule. +- **Passwords are read from files, never taken as flag values.** +- **The voice wire is plaintext with no auth.** mavend's voice port stays on + homesrv loopback and reaches workpc over ssh. Do not LAN-bind it. + `SurfaceVoice` caps acts at L0, and L0 does not cap reading. +- **A GPU service added beside mavgpud goes in `cmd/mavgpud`, never in systemd.** + The card needs one owner. A second unit made mavgpud evict llama-server every + few seconds and took the model arm down for eight minutes. ## The ecosystem: Nexus, Praxis, Hexis -Maven is one of four services. It owns conversation and personal memory. It does -not own identity, operational state, or execution. Full contract in -`docs/ecosystem.md`. - -```text -Nexus identifies. Praxis observes. Hexis acts. Maven understands and coordinates. -``` - -| Service | Owns | Maven's client | Configured at | -|---|---|---|---| -| **Nexus** | Canonical entity ids, names, aliases, relationships. Projects, services, devices, people, pets, places. | `nexusClient` in `cmd/mavend/ecosystem.go`, `POST /api/v1/resolve` | `nexus.url` (`http://nexus:9740`) | -| **Praxis** | Operational attention and item lifecycle. What needs looking at, what changed, what is unresolved. | `praxisClient`, the HTTP tools API under `/api/v1/tools/` | `praxis.url` (`http://praxis:8989`) | -| **Hexis** | The capability registry and the only path to executing anything. | vendored `github.com/kami/hexis/pkg/client` | `hexis.url` (`http://hexis:9741`) | - -All three are `nil` unless configured and every one degrades on its own. An -outage means a named gap in the answer, never a broken turn and never a guess. - -Rules that are not negotiable: +Nexus identifies, Praxis observes, Hexis acts, Maven understands. Maven does not +own identity, operational state, or execution. Full contract in +`docs/ecosystem.md`. All three are `nil` unless configured and each degrades +alone. An outage means a named gap, never a broken turn or a guess. - **No component reads another component's database.** Praxis attention comes over HTTP, never from its SQLite file. -- **Identity lives in Nexus.** Do not invent a local fact key for something Nexus - resolves. `actionFact` sets `Subject`, and `cmd/mavend/factenrichment.go` - resolves it in the background. +- **Identity lives in Nexus.** Do not invent a local fact key for something + Nexus resolves. `cmd/mavend/factenrichment.go` resolves `actionFact.Subject`. - **Free text never reaches a mutating Hexis call.** Resolve to a canonical entity id first. Ambiguous resolution asks the owner, it does not pick. - **LLM output is not authorization.** Confirmation binds capability id, target - entity, arguments, requester and expiry. See `cmd/mavend/confirm.go`. -- **Praxis lifecycle words mean different things.** Surfaced is not acknowledged, - acknowledged is not resolved, execution success is not recovery. Reading an - item aloud calls `Surface`, never `Acknowledge`. -- **No automatic attention-to-action path.** Digestion may summarise Praxis. It + entity, arguments, requester and expiry (`cmd/mavend/confirm.go`). +- **Praxis lifecycle words differ.** Surfaced is not acknowledged, acknowledged + is not resolved, execution success is not recovery. Reading an item aloud + calls `Surface`, never `Acknowledge`. +- **No automatic attention-to-action path.** Digestion may summarise Praxis and may not call Hexis. - -Every cross-service call carries a correlation id minted once per action -(`withCorrelationID`), a contract version header, and `X-Requested-By: maven`. +- Every cross-service call carries a correlation id minted once per action + (`withCorrelationID`), a contract version header, and `X-Requested-By: maven`. ## Routing **Read `docs/routing.md` before touching `internal/router/` or `queryWalk`.** It -carries the stage-by-stage reasoning, every measurement, and why each rule -exists. What follows is only what must not be broken. +carries the reasoning, the measurements and every rule's why. A route produces +two decisions. **Intent** is one of seven values. **Source** is where the answer +lives and is read on `IntentQuery` alone. Score them separately. The cascade is +stage 0 grammars, then the routing heads, then the resident model, then the +classifier. Every stage may decline and the next one answers. -A route produces two decisions. **Intent** is one of seven values. **Source** is -where the answer lives and is read on `IntentQuery` alone. They are scored -separately, because one number hides which one moved. - -The cascade is stage 0 grammars, then the routing heads, then the resident model, -then the classifier. Every stage may decline and the next one answers. - -- **The classifier is the floor, not dead code.** It runs when the resident model - is off. It runs when there is no llama-server, and on any error. Deleting it - makes a model outage a broken turn. -- **Any model error falls through**, so a turn never breaks on a model. -- **`baselineGrammars` in `eval_test.go` mirrors `buildRouter`.** Add a grammar - to one and it belongs in both, or the fixture scores a set nobody runs. +- **The classifier is the floor, not dead code.** It answers when the resident + model is off, absent, or erroring. **Any model error falls through.** +- **`baselineGrammars` in `eval_test.go` mirrors `buildRouter`.** A grammar + added to one belongs in both, or the fixture scores a set nobody runs. - **Go's `\b` is ASCII-only** and never fires after a Cyrillic letter. A Russian pattern needs an explicit `(\s|[?!.]|$)`. -- **`PraxisGrammars()` is the only path to Praxis**, not a faster one. The model - reaches Praxis 0/12 alone, because nothing in the router prompt names a Praxis - capability. -- **`voice.embedder.heads_path` must never point at `model_path`.** The resident - e5-small must not be replaced by the fine-tuned copy. Recall depends on that - file scoring what it scored. Fine-tune a copy of the weights. -- **Bump `tokenizerRev` on any change to what `encodeWord` emits.** The embedder - id carries the revision. So a tokenizer fix triggers `ReembedAll` the way - swapping the model file does. -- **A new rung in the `runTurn` ladder needs its name in `preRouteLadder`** - (`cmd/mavend/decisiontrace.go`). Otherwise that rung is silently missing from - the decision record. +- **`PraxisGrammars()` is the only path to Praxis**, not a faster one. +- **`voice.embedder.heads_path` must never point at `model_path`.** Recall + depends on the resident e5-small scoring what it scored. Fine-tune a copy. - **Routing traces are retained 14 days**, enforced on write and again on start. - The utterance is stored in clear and nothing reads it outward. `Store.Wipe` - deletes it with everything else. +- **Bump `tokenizerRev` on any change to what `encodeWord` emits**, so a + tokenizer fix triggers `ReembedAll` the way swapping the model file does. +- **A new rung in the `runTurn` ladder needs its name in `preRouteLadder`** + (`cmd/mavend/decisiontrace.go`), or it is missing from the decision record. -### `queryWalk` and the destination +**`queryWalk` takes query sources out and moves none** (`actions_query.go`). That +is the safety argument and it is not negotiable. The table's order is +load-bearing and carries "the owner's data first, then the world". +`SourceUnknown` is the floor and walks the whole chain. A named destination +removes only the sources marked `guesses: true`, so a source that looks rather +than guesses is always asked. **The personal boundary is the one exception and +it is deliberate.** It guesses, so naming `SourceWorld` drops it. **Only a stage +0 grammar may drop it** (owner's call, V-666). `queryWalk` reads +`Decision.SourceAnchored` for the source marked `boundary: true` and no other. -`queryWalk` in `cmd/mavend/actions_query.go` takes sources **out** and moves -none. That is the safety argument and it is not negotiable. The table's order is -load-bearing, and above all it carries "the owner's data first, then the world". +Judge a routing change against the classifier (76.0% intent, 36.4% destination) +and the resident model (80.2% intent), since those always answer. The fixture +has grown from 77 cases to 96, so a number compares only to another number on +the same fixture. -`SourceUnknown` is a real value and it is the floor. Nothing named a destination, -so the daemon walks the whole chain. Naming `SourceWorld` does not send the turn -outside on its own. +## Language: model output and Russian -What comes out is only the sources marked `guesses: true`. Those decide a turn is -theirs by cosine against frozen seeds, then answer whatever they claimed. A -source that looks rather than guesses is always asked. +Both contracts are in `docs/language.md`. What must not be broken: -**The personal boundary is the one exception and it is deliberate.** It guesses, -so naming `SourceWorld` drops it. **Only a stage 0 grammar may drop it** -(owner's call, V-666). `Decision.SourceAnchored` carries the provenance, and -`queryWalk` reads it for the source marked `boundary: true` and no other. So -every other guesser still comes off the turn, whoever named the destination. -`TestOnlyAGrammarMayDropTheBoundary` and `TestNamingRecallKeepsTheBoundary` pin -both directions. - -### Current numbers - -| Arm | Intent | Destination | p50 | -|---|---|---|---| -| classifier + ONNX | 76.0% | 36.4% | 16.6µs | -| resident Qwen3-1.7B, cascade | 80.2% | not measured | 1.19s | -| routing heads, cascade | 96.9% | 75.8% | 27.9ms | -| workstation gemma-4-E4B, cascade | 89.6% | 57.6% | 294ms | - -Judge a routing change against the classifier and the resident model, since those -are what always answer. The fixture has grown from 77 cases to 96, so a number is -comparable only to another number on the same fixture. - -## LLM output contract - -All phrasing paths emit `{"response":"...","mood":"..."}`, falling back to plain -text when the model skips the JSON. **One parser, `parseResponseMood` in -`internal/phraser/parse.go`**, and every path reaches it: the six `LLMPhraser` -methods, `PhraseWorld`, and `Replier.PhraseReply`. `cmd/mavend/replier_llm.go` -wraps the last of those, holds the stub fallback, and does no parsing of its own. -Mood is a fixed enum. - -The router prompt is a separate contract: -`[{"intent":, key?, value?, text?, verb?}, ...]` over 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 stay identical. - -## Russian patterns: three mechanisms, no fourth - -Hand-written Russian stem patterns were swept out on 2026-08-04 (owner's call). -A regex whose output is a fact or a route is the defect. A regex over structured -input, such as HTML, MIME, JSON, a URL or an argv list, is not. Before writing a -Russian word list, pick one of these: - -- **`internal/lexicon`** for closed classes, in `lexicon_ru_v1.json`. - Interrogatives, capture verbs, reminder verbs, cardinals, day offsets, parts of - day, weekdays, months, spoken hours. Editing a word is a data change and there - is exactly one copy. Months used to live in three files. Cardinals carry the - oblique forms, because a spoken time declines and `в семь` and `к семи` are one - hour. -- **`internal/morph`** for grammar, from the vendored golem Russian dictionary. - `IsVerbForm` and `SameWord`. Lemma matching is BROADER than stem-plus-one-ending, - so a verb slot meaning the imperative must be matched exactly. `говори` and - `говорил` are one lemma and only one of them is a command - (`cmd/mavend/quiet_toggle.go`). -- **`cmd/mavend/topics.go` and the embedder** for open sets, where the question is - what a turn is ABOUT. Frozen seeds per subject plus a real `other` class, scored - against the turn's own query vector. Same shape as the personal boundary in - `personalboundary.go`, with one difference. A topic must clear the runner-up by - `topicMargin`, because a false claim here spends a network scan rather than one - honest "не знаю". The old keyword tests stay as the offline floor. -- **The ecosystem trio** when the answer is not in the utterance at all. Identity - is Nexus's, never a local pattern. - -Seeds are scoring data. Editing one moves a recogniser and must be re-measured -against the `TestONNX*` tests, not eyeballed. +- **One parser for model text, `parseResponseMood`** in + `internal/phraser/parse.go`. Every phrasing path reaches it. Mood is an enum. +- **The router prompt is a separate contract** over 7 intents, and + `llm/check_prompt_parity.py` keeps the Go and relabelling copies identical. +- **Russian words are matched by three mechanisms and no fourth**: + `internal/lexicon` for closed classes, `internal/morph` for grammar, and + `cmd/mavend/topics.go` with the embedder for open sets. A regex whose output + is a fact or a route is the defect. A regex over structured input is not. +- **Seeds are scoring data.** Editing one moves a recogniser and must be + re-measured against the `TestONNX*` tests, not eyeballed. ## Non-goals and hard constraints Not a nag, not autonomous. - -**The persona is feminine.** Russian self-reference uses feminine forms: `рада` +**The persona is feminine.** Russian self-reference takes feminine forms: `рада` not `рад`, `поняла` not `понял`. The owner is male and she speaks to him -informally. Use "ты", singular, never "вы" or "ваш", and never "он" or "его". She talks TO -the owner, not about him. Pet names such as "милый" are forbidden. The name -"Ками" is not. `CheckAddress`, `CheckFeminine` and `CheckCringe` in +informally. Use "ты", singular, never "вы" or "ваш", and never "он" or "его". +She talks TO the owner, not about him. Pet names such as "милый" are forbidden. +The name "Ками" is not. `CheckAddress`, `CheckFeminine` and `CheckCringe` in `internal/phraser/eval/checks.go` enforce this, scored by `make eval-phrasing`. -**"Never phones home" is deprecated** (owner's call, 2026-07-31). A 1.7B does not -know enough to answer world questions, so she reads external sources. What -replaces it: +**"Never phones home" is deprecated** (owner's call, 2026-07-31). She reads +external sources, and `docs/world.md` carries that chain. What holds regardless: -- **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. +- **No telemetry, no cloud model, no third-party account.** Inference stays on + the box and nothing about Maven is reported to anyone. - **The owner's data first, then the world.** Every source reading his facts, - notes, calendar, tasks or house runs before anything outside. The personal - boundary sits between them. Reading beats recalling for a small model. -- **The owner's notes and facts are never search input.** Only the utterance goes - out. Never the persona block, the history, or matched notes. -- **External search is allowed and off unless configured**, like weather and - telegram. The code default is off. `deploy/mavend.json` ships a `search` block, - so it is on for this box and deleting the block turns it off again. -- **In the world, live search leads and the ZIMs are the fallback** (owner's - call, 2026-08-02). A self-hosted SearXNG answers first. The Kiwix ZIMs on - homesrv answer when the search is empty, unreachable, or the line is down. - -### The world chain - -`Response.Empty()` is the whole gate and there is no quality threshold in front -of it. Four signals were tried and none separates a real question from an -invented one. Token overlap would cost "столица Франции" its answer, because the -answer is Париж and that word is not in the question -(`docs/evals/2026-08-05-search-quality-signals.md`). **The embedder is not a -fifth signal**: query-to-passage cosine measures topic and not whether the -passage answers, and the two sets overlap -(`docs/evals/2026-08-09-kiwix-topic-retrieval.md`). - -The connect phase alone is capped at `dialTimeout` (1.5s), because a blackholed -host once cost the owner 8 seconds. A slow instance that did connect keeps the -full 8 (`docs/evals/2026-08-05-kiwix-offline-fallback.md`). - -**A Russian question reads `wikipedia_ru_all_maxi_2026-02` verbatim** through -`kiwix.book_ru`. The RU→EN rewriter is the workaround for an English book and is -skipped there. Kiwix catalog names come from the filename, not the `` -field. - -**Kiwix ranks by keyword overlap.** Never send it a whole sentence. -`kiwix.Topic` drops the narrative request, the interrogative and a verb behind -one. `kiwix.TitlePath` tries the exact article first, since a ZIM is addressable -by title and a wrong title is a 404. `TitleCandidates` tries the spoken form and -then the capitalized one. Both apply on the verbatim path alone. The rewriter -already reduces a question, and reducing twice takes the topic off its input -(V-668). - -**Which query source claimed a turn is readable on `/chat`** as a badge beside -the reply. It is carried on `ipc.ChatReply.Source` and noted by `noteQuerySource` -in `cmd/mavend/querysource.go`. It rides the context, so `handleText` keeps the -one string signature the mic, telegram and the web share. - -## Web UI conventions - -Server-rendered pages share `cmd/mavweb/static/ui.css` (served at `/ui.css`) and -the shell partial in `cmd/mavweb/shell.html`. A page opens with -`{{template "shellTop" ""}}` and closes with `{{template "shellBottom"}}`, -and the key marks the active sidebar link. - -Every page is its own embedded `.html` file next to `main.go`. No page markup -lives in Go, and the sidebar is data (`sidebarSections`, `pageIcon`) the template -renders. No per-page `