Files
Maven/PROGRESS.md
T
kami 5d1850c203 fix: mavwaked is a client binary, not a docker daemon
- Revert Dockerfile: no mavwaked build, no alsa-utils runtime dep
- Revert .dockerignore: no /mavwaked entry
- PROGRESS.md: deploy note says systemd user unit on a client box
  (desk PC, pi), connects to mavend over wg or local net — never on
  the homesrv or in docker
2026-07-06 14:12:33 +04:00

23 KiB
Raw Blame History

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 17 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 (sev12 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:<name> 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 systemd user unit on a client box (desk PC, pi, etc.) where the mic is attached. Connects to mavend over wg or local net via -addr. Deferred until a client box is wired with a mic.

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. Runs on client boxes (not docker/homesrv) via systemd user unit; connects to mavend over wg or local net.

  • 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 13 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 reminderscron + 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 engineExplainTick/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/undoRevertFact voids the latest fact for a key (append-only void-marker, audit trail intact); exposed at /api/revert from /history.
  • Tool scopesscope 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).
  • Housekeepingdb_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 /dashRecentNotes 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: the mic lives on a client box (desk PC, pi, etc.) — never the homesrv. Deploy action: systemd user unit on whichever box has the mic, connects to mavend over wg or local net.
  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:

  1. 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.
  2. 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.
  3. Routing quality depends on the ONNX embedder being configured — the HashEmbedder floor makes RU recall lexical/weak; many commands fall to "clarify".
  4. Presence is effectively one signal (page_heartbeat); desk_active is still an undeployed script — "voice when near" routing runs on a guess.
  5. 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 23 (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.