## Maven — current state (2026-07-06) Consolidated status. The reactive↔proactive core is closed and testable through the web PWA. The SPEC's open items 1–7 are landed (protocol doc, away-channel fallthrough, CalDAV poller, quiet-hours schedule, tools enable/disable, note RAG, passkey step-up); item 8 (multi-user) is deliberately deferred — see the tail. The two big infra gaps from the jul5 revision are closed on `overnight-jul5`: **at-rest encryption** (AES-256-GCM, tmpfs working copy — not sqlcipher, see `internal/store/crypt.go`) and **Docker deployment** (one image, six daemon containers). The `overnight-jul6` session (now on `master`) closed the biggest *query-surface* gaps — **calendar querying, general-knowledge answers, and weather** — plus a populated homelab act allowlist and two pure scaffolds (dialogue state, long-term-memory vector store). ~15.2k LOC + ~8.5k test, 303 tests, `-race` in `make test`. ### Access model - **Phone** → needs the wg tunnel to reach homesrv (no homesrv DNS otherwise; raw IP or a DNS tweak can bypass, not the default). - **PC** → uses homesrv DNS, resolves the domains over local-net, **no wg needed**. - nginx + ufw both scope to `10.42.0.0/24` (wg) + `192.168.1.0/24` (LAN), deny all else. - **Surface in use now: the web PWA (`mavweb`).** Voice PTT + in-app nudges both ride it. ### Works end-to-end (tested) - **Reactive voice:** PWA record → Whisper STT (`mavsttd`) → ONNX classifier → LFM 2.5-1.2B phraser (llama-server subprocess) → Piper TTS (`mavttsd`) → reply. HTTP POST path (mobile-Chrome drops WS for the audio). - **Capture:** `fact` (EN **and RU** — root-substring recognizers) + `reminder` persist through CoreAPI (`source=tap:voice`). This is the substrate the care rules read. - **Notes / query (semantic recall, sqlite — no chroma):** `note` → embed (the classifier's ONNX embedder) → `notes` table. `query` → embed → brute-force cosine top-k → confidence-gated (below `queryMinScore` 0.55 ⇒ "no note", not a guess). **Note RAG (SPEC item 6):** the gated top-k feed the phraser (`PhraseQuery`) to compose a natural answer ("вот что я нашла: …") instead of a verbatim dump; raw-notes fallback on any LLM error. Stub is deterministic. - **Monitoring (`/dash`):** mavweb server-renders presence + recent nudges (by outcome) + recent facts from the append-only store via CoreAPI. Read-only, meta-refresh, no JS. - **Proactive loop:** 60s dumb ticker, pure predicates over a State snapshot, universal gate (quiet-hours/presence/cooldown/snooze/calendar), one-nudge-per- tick max-severity, reminders (gate-bypassing), sev4 repeat-til-ack, feedback auto-tuner (outcome ratio → bounded cooldown, persisted as `source=feedback`). - **Rules:** water/meal/break (sev1–2 care), service_down (sev4, `poll:uptimekuma`), netdata_critical (sev3, `poll:netdata`). - **Routines (`internal/routine`):** operator-declared clockwork — the third proactive class beside reminders (user-stated) and care rules (world-state). Config `routines[]` (cron + literal RU body + severity) fire through the normal dispatcher on schedule (an 08:00 briefing, a 22:00 wind-down). Bodies are literal (not LLM-phrased ⇒ can't hallucinate); rule name `routine:` so they don't pollute the care autotuner; cold-start guard seeds on first sight so a restart never replays a missed schedule. Pure `routine.Due`, unit-tested; the tick driver holds the last-fired map. - **Env facts (`mavpoll`):** netdata alarms → `netdata_alarm` (fires immediately on a real CRITICAL); kuma monitor_status → `service_down`. Writes only on value-change (no append-only churn). - **Presence:** noisy-OR decay + Schmitt hysteresis. Live via `page_heartbeat` (PWA auto-pings `/api/signal` every 30s → present when a tab's open). - **Delivery:** ntfy / telegram / voice by `f(severity, presence)`; minimal body on away channels. PWA subscribes to ntfy over **WebSocket** for in-app nudges. - **Away-channel fallthrough (SPEC item 2):** when the router picks voice but no live session exists at push time (presence guess was wrong), the dispatcher reroutes through the AWAY table — sev3→ntfy, sev4→telegram-repeat-til-ack, sev≤2→drop — instead of silently dropping. Covers nudges + reminders. - **Calendar busy (SPEC item 3, `mavcaldav`):** new poller queries a self-hosted **Radicale** CalDAV server on an interval, writes `calendar_busy` + event facts through CoreAPI (value-change only). The loop gate already consumes `calendar_busy`. - **Quiet-hours schedule (SPEC item 4):** the gate reads `quiet_hours`; a config time window (`voice.quiet_hours`, HH:MM, midnight-crossing handled) now sets it on each tick — in addition to the "тихий режим" voice toggle. Both activate quiet. - **Client protocol (SPEC item 1):** the voice wire format (length-prefixed JSON frames) is published in `PROTOCOL.md`, generated from `internal/voice/wire.go` so third-party clients don't need the Go source. - **Passkey step-up (SPEC item 7):** `internal/webauthn` does real WebAuthn — ES256/P-256 register + assert, ecdsa signature verification, rpIdHash + UP/UV flag binding (UV = the gesture), sign-count regression check. `PasskeySession` bumps the auth session L2→L3 for a TTL on assert. mavweb serves `/auth/passkey` (enroll + step-up) + the begin/finish endpoints. Crypto is round-trip tested (incl. tampered-sig / missing-UV / wrong-origin negatives). - **Stability:** llama-server orphan leak fixed (`Pdeathsig` kills the child on any mavend death); `kill-maven.sh` reaps strays (matches the model, not a bogus `llama-server.*maven` pattern); `start-maven.sh` wires `-core` + poller. ### Wired but needs a deploy action (not code) - **`desk_active`** (strongest presence signal) — `scripts/desk-active.sh` runs on the **desk PC** (hypridle-gated systemd timer), posts over wg to mavweb. - **Kuma `service_down`** — needs an API key created in Kuma → Settings → API Keys, passed to `mavpoll -kuma-key`. - **`mavwaked`** (always-on listening) — needs a docker-compose service entry with audio device passthrough (`/dev/snd` + `group_add: audio` on the container, or run as a systemd unit on the host for lower ALSA latency). Deferred until the USB mic is on the homesrv and tested. Caveats / gotchas: - **desk_active is a workstation deploy, not code** — 0 facts ever written; presence runs on page_heartbeat alone (dash reads "away"/"never at desk"). `scripts/desk-active.sh` + a hypridle-gated `maven-desk` timer must be installed on the desk PC (not homesrv). - **Notes recall needs the ONNX embedder** — under the HashEmbedder floor, cosine is lexical (token overlap), not semantic; scores are low, so most RU commands sit under the 0.35 route threshold and clarify. Configure `voice.embedder` for confident recall+routing. (The floor now at least tokenizes Cyrillic — see below — so it ranks correctly, just weakly.) - **Switching the embedder model silently breaks old notes** — different dim ⇒ cosine 0 ⇒ they stop matching; brute-force can't re-embed. Re-embed on a model change. - **`wg_handshake` is OFF and should stay off** — in this topology the phone only runs wg when *outside*, so a fresh handshake means AWAY, not here. The `mavpoll -wg` flag exists (defaults `""`) and could later back the spec's "away override" by flipping the sign; as a presence-*here* signal it's inverted. desk_active + page_heartbeat cover home presence. ### Done since last revision (overnight-jul6, 2026-07-06) Seven tasks (`SESSION-06-07-2026.md`), one commit each, merged to `master`. This session was run through **opencode**, not Claude Code (co-author trailer). Since then (**2026-07-06, second session**): - **Always-on listening (gap 1, MVP)** — `cmd/mavwaked/`: 825 lines, 10 `-race` tests. Energy-based VAD over 30ms windows (same RMS threshold as mavsttd's `gateReason`), adaptive noise floor, speech→silence state machine. Captures PCM from arecord(1) subprocess, sends `PushToTalk` with `Surface=SurfaceVoice` (L0 — no destructive acts). Reply plays through aplay(1). No wake word yet (pure VAD trigger); the 30ms frame shape matches silero-vad ONNX input 1:1, so swapping energy-threshold for ONNX inference is a local change in vad.go. `Makefile` `build-waked` target; Docker image includes `alsa-utils` + binary. - **Calendar querying (task 3)** — "что у меня завтра?" now answers from the CalDAV facts the poller already writes. Added `store.CalendarEvents(from,to)`, a RU date-scope parser («сегодня»/«завтра») in `router/slots.go`, and an IPC `CalendarEvents` RPC (api/client/server/wire) feeding the `IntentQuery` handler. Empty day → «на сегодня ничего нет». Previously calendar only *gated* nudges; it's now queryable. - **General-knowledge routing (task 4)** — when notes-RAG misses `queryMinScore`, the query now falls through to the phraser with an anti-hallucination system prompt (`router.KnowledgePrompt`, single tested source) instead of giving up. Empty/errored/Stub phraser → «не знаю.», never a fabrication. - **Weather (task 5)** — new `internal/weather/`: `Provider` interface, a stub («погода не настроена»), and a real **keyless Open-Meteo** provider (geocode + current_weather, injectable `*http.Client`, mocked in tests — no live network). Wired into `IntentQuery` (keywords погода/градус/температура) with a ~5s context timeout; selected by `voice.weather.provider` ("open-meteo" | "" → stub). - **Homelab act allowlist (task 2)** — `voice.tools` seeded with read-only acts (`systemctl status`, `docker ps`, `uptime`, `df`, `free`, `journalctl` reads) as `destructive:false` and mutating ones (restart/stop/start/reboot, docker-restart/stop) as `destructive:true`. Guardrail verified: no dangerous verb is `destructive:false`. RU phrasings seeded in `act.txt`. - **Embedder config validation (task 1)** — a partially-filled `voice.embedder` block (some of model/tokenizer/lib paths missing) is now a load error instead of a silent fall-through to the Hash floor; the floor fallback logs explicitly. - **Dialogue state scaffold (task 6)** — `internal/dialogue/`: `Session` + TTL `SessionStore` + pure `InheritSlots`. **Now wired** (post-merge follow-up): the voice handler carries slots across same-intent turns within a 2-min window (`followUpMerge`, unit-tested) — bounded gap-filling, not full multi-turn yet. - **Long-term memory interface (task 7)** — `internal/memory/`: `Store` interface + `InMemoryStore` (cosine). Wired into `IntentNote` (best-effort insert) and, post-merge, into `IntentFact` (facts indexed) + `IntentQuery` (read-back after notes-RAG misses). In-memory only — no persistent backend yet (gap #8). Follow-ups (Claude Code, post-merge): gofmt'd `handlers_test.go` (the jul6 verification commit left it misaligned, so `gofmt -l` still flagged it despite the "all gates green" claim); deduped the task-4 knowledge prompt to the single tested `router.KnowledgePrompt()`. Tree is now genuinely green (gofmt/vet/303 tests). ### Done since the jul5 revision (overnight-jul5, 2026-07-05) The overnight session (`SESSION-05-07-2026.md`, 25 tasks) closed the previous "not built yet" items 1–3 and added feature depth: - **At-rest encryption** — the on-disk db is AES-256-GCM ciphertext; the daemon works on a tmpfs (RAM) plaintext copy, sealed back atomically on close. Wrong key / tamper ⇒ fail closed, never a plaintext fallback. Legacy plaintext dbs upgrade on first clean shutdown. Key via config/env (`db_key_env`); no KDF — raw 32-byte key, base64. The passkey cold-start unlock plugs into the same `store.OpenEncrypted` seam later. - **Docker deployment** — single image, one container per daemon (`docker-compose.yml`); only mavend mounts the key + db volume; IPC over a shared socket volume. `ipc.DialWait` (boot-order tolerance) + redial-on-drop (core restarts don't kill modules). `deploy/README.md` has the runbook. - **Tests** — mavcaldav, mavttsd, voicesink, mavweb main/handlers covered; `make test` runs `-race -coverprofile`. - **Recurring reminders** — `cron` + `next_fire_ts` on reminders; recurring ones reschedule (instead of mark-fired) after successful delivery. - **Notification digest/batching** — low-severity nudges queue and flush as one digest per window/max-items (`digest` config block); stale-reminder bursts on boot collapse into a single digest reminder, completed only after delivery. - **Rule trace engine** — `ExplainTick`/`ExplainGate` record per-rule predicate/gate/selection results each tick; served over IPC (`tick_trace`) and rendered at mavweb `/trace` ("why didn't she nudge me"). - **Web UI** — new `/history` (facts + revert buttons), `/notifications` (nudge history), `/trace` pages; nav links on `/dash`; RU/EN cheatsheet toggle in the PWA; manifest icons (`icon.svg`). POST `/tools` now requires an in-process passkey step-up when WebAuthn is configured. - **Revert/undo** — `RevertFact` voids the latest fact for a key (append-only void-marker, audit trail intact); exposed at `/api/revert` from `/history`. - **Tool scopes** — `scope` column on tools, threaded through propose/enable/UI. `DisableTool` raised to AuthStepUp alongside Enable. - **Passkey persistence** — mavweb credentials in a JSON file (`-passkey-file`), surviving restarts; rollback-on-persist-failure keeps memory and disk in sync. - **STT silence gate** — min-duration + RMS floor drop non-speech before whisper hallucinates on it (`-min-ms`, `-silence-rms` flags on mavsttd). - **Housekeeping** — `db_key.env` gitignored (+`.env.example`), `build-caldav` target, zero-timestamp "never" fix on /dash. ### Not built yet (ranked by ROI) 1. **Cold-start unlock** — the at-rest key still comes from env/config; the passkey→key L3 dance is a documented seam, not a feature. Until then the key sits in the container env. 2. **Multi-user (SPEC item 8)** — deliberately deferred, see the tail. Closed (jul6 follow-ups): `/api/revert` now sits behind the same passkey step-up as POST `/tools`; `go.mod` direct deps (`onnxruntime_go`, `coder/websocket`, `robfig/cron`) are labeled correctly — `go mod tidy` can't run here because it walks the vendored `deps/go` toolchain tree. Purge+rotate leaked db key (#12) — investigated and closed: the key was **never committed** to git history (gitignored at introduction, no commit ever tracked `deploy/db_key.env`), so nothing to scrub. File stays on disk and in deploy env by design — at-rest encryption needs it at boot. Done earlier (2026-07-03): **act tool executor, store-backed, full flow** (`internal/tool` + `internal/store/tools.go` + `tools` CoreAPI methods). - **Execution:** IntentAct runs the matched fn against the store's ENABLED allowlist. argv, no shell → STT text can't inject. Live store read, so a newly-enabled tool runs without a daemon restart. - **proposed→enabled→disabled (SPEC item 5):** an act whose verb isn't enabled is scaffolded as a `proposed` tool (maven suggests). A human enables it (fills argv + destructive) on the authed **`mavweb /tools`** page — never voice — and can disable it back to `proposed` (kept in the store, won't run). `EnableTool`/ `DisableTool` sit at `AuthStepUp`; the gate is now **live** via `PasskeySession`, so /tools enable requires a passkey assertion at `/auth/passkey` first. - **Confirm turn:** a destructive enabled tool replies "выполнить X? да/нет" and parks; the next utterance (ru/en yes-no) confirms or cancels (90s TTL). - **Config:** `voice.tools` seeds enabled tools at boot (editing mavend.json = the human enable act); mavweb enables ad-hoc ones on top. - **Russian:** fixed grammar in reply strings + seed files; maven's self- reference is feminine ("she") — [[maven-persona-gender]]. Also fixed: - **HashEmbedder was blind to Cyrillic** (`tokenize` iterated bytes, kept only `a-z0-9`) → every RU utterance embedded to the zero vector → cosine 0 across all intents → misrouted to `act` (alphabetical tie-break). Now rune-based (`unicode.IsLetter`). This was the real cause of "Найди заметку" (a query) landing in `notes`; added note-retrieval query seeds too. - **Notes are now browsable on `/dash`** — `RecentNotes` plumbed through the store + CoreAPI; voice-captured notes were previously only reachable via semantic `query`. Earlier: notes/query recall, `/dash` monitoring, `wg_handshake` poller (NO-OP). ### Gaps — why "voice assistant" is still aspirational (2026-07-06) What separates Maven today from the thing the spec describes. Dealbreakers first — these define the category: 1. **Always-on listening is code-complete (MVP).** `cmd/mavwaked` captures PCM from arecord → energy-based VAD → PushToTalk with `Surface=SurfaceVoice` (L0). Gap narrowed: no wake word yet (pure voice-activity trigger; every utterance fires). The 30ms frame shape and 16kHz PCM match silero-vad's ONNX input exactly, so a wake-word model swap is a local change in vad.go. Hardware: needs a mic on the always-on box (USB mic on homesrv or the desk PC). Builds and tests; deploy action pending (docker-compose service + audio device passthrough). 2. **Conversation is thin, not absent.** The router still classifies one utterance → one reply and there's no anaphora resolution or LLM-driven dialogue. But `internal/dialogue` is now **wired** (jul6 task 6 + follow-up): a 2-min session carries slots across turns, so a same-intent follow-up («напомни завтра» → «…позвонить маме») inherits the earlier time. Bounded to same-intent gap-filling — cross-intent anaphora and real multi-turn dialogue are still future. The sub-1B phraser only words replies. 3. **Latency/shape of a turn.** Clip-based STT (record → upload → whisper → route → phrase → piper → play). No streaming either direction, no barge-in; every exchange is a full round trip. Capability-class gaps — built but thin: 4. **Act surface is a small argv allowlist.** propose→enable works and the allowlist now ships a homelab starter set (jul6 task 2 — status/ps/uptime/ df/free/logs read-only, restart/stop/reboot gated). Still bounded to what's seeded; broadening it is config, not code. 5. **Query answers now cover notes + calendar + weather + general knowledge** (jul6 tasks 3/4/5). Calendar querying, keyless Open-Meteo weather, and a phraser knowledge-fallback all landed; caveat — general-knowledge quality is only as good as the sub-1B phraser, and weather needs `voice.weather.provider` set. The cheatsheet and router are now roughly aligned. 6. **Routing quality depends on the ONNX embedder being configured** — the HashEmbedder floor makes RU recall lexical/weak; many commands fall to "clarify". 7. **Presence is effectively one signal** (page_heartbeat); desk_active is still an undeployed script — "voice when near" routing runs on a guess. 8. **Long-term memory is now persistent (store-backed), not the spec's chroma.** `internal/memory` has a `Store` interface; the daemon now wires `store.MemoryStore` (`internal/store/memory.go`) — a **persistent** backend in the **same encrypted sqlite db** (survives restarts; recall text inherits at-rest encryption, so no plaintext sidecar). Vectors are float32 blobs, search is brute-force cosine (fine at single-user scale; ANN is the later swap behind the same interface). Notes **and facts** are indexed on capture; `IntentQuery` reads it back (after notes-RAG misses, before general-knowledge) — fact recall («когда я пил воду?») is its distinct payoff. The in-memory impl remains the test/no-store floor. Remaining: an ANN/external index is optional-scale, not a gap. Persona prompt and custom TTS voice (kami-picked, replaces the irina floor — [[custom-voice-training]]) are still future items. Ops footnote: voice-over-web verified 2026-07-06 — mavend binds 0.0.0.0:9100 and mavweb reaches it cross-container at mavend:9100 (nc -z confirmed). mavpoll uses network_mode=host to reach localhost services (netdata, kuma). ### Future / logged, not now Personality prompt; custom TTS voice training (kami-picked voice, replaces irina floor); listening modes 2–3 (meeting-record, ambient-derive). ### Services & layout - `mavend` (core, IPC unix socket) — store + loop + phraser; the only key-holder. - `mavsttd` / `mavttsd` — STT/TTS worker modules (unix sockets). - `mavweb` — PWA bridge (HTTP), `/api/ptt` voice, `/api/signal` presence ingest, `/api/ntfy` WS-subscribe config, `/dash` read-only monitoring. - `mavpoll` — env poller (netdata/kuma → facts via CoreAPI). - `mavcaldav` — CalDAV poller (Radicale → `calendar_busy` + events via CoreAPI). - All behind wg + nginx deny-all; no phone-home. CGo only in `mavsttd`. - Start/stop: `./start-maven.sh [build]`, `./kill-maven.sh`. - Config: `~/.config/maven/mavend.json` (or `mavend.json` in repo root). ### Key files - `cmd/mavend/{main,tick,voice}.go` — daemon wiring, loop driver, voice handler - `internal/loop/{loop,rules,gather,feedback}.go` — proactive engine - `internal/store/` — append-only facts/reminders/nudges/presence/notes - `cmd/mavweb/{main.go,dash.html}` — PWA bridge + `/dash` monitoring - `internal/router/{classifier,slots,stage0}.go` — reactive routing + slot parse - `internal/delivery/` — dispatcher + ntfy/telegram/voice sinks - `internal/auth/` — scope/gate/policy; `FloorEnrollment` (same-uid = device trust) + `webauthn.PasskeySession` (real step-up for L3) - `internal/webauthn/`, `cmd/mavweb/webauthn.go` — passkey register/assert - `cmd/mavcaldav/`, `cmd/mavpoll/`, `scripts/desk-active.sh` — env producers ### Why multi-user (SPEC item 8) is deferred Not neglect — the one item where doing nothing now beats doing something: - **No second user exists yet** (the "gf phase"). Building per-user partitioning now means code exercised by zero users and validated by nobody — YAGNI. - **The append-only schema makes it a migration, not a rewrite.** No row is ever mutated, so adding `facts/notes/reminders.user_id` later is add-columns + backfill-to-"kami" — no reshaping, no dual-write window. Deferral is cheap. - **The hard part is speaker attribution, and it needs the second voice.** A voice-print discriminator (kami vs gf vs unknown) can't be trained or tuned with one voice in the house. Plumbing before the model is pipe with no water. - **It's fenced deliberately** (`DO NOT TOUCH THIS PHASE` in SPEC.md) so an autonomous agent doesn't add `user_id` columns while touching the store and commit us to a schema before the constraints that shape it exist.