The doc was from the initial commit and predated all open-item work. Update the works-end-to-end list (protocol doc, away-fallthrough, mavcaldav, quiet-hours schedule, tools enable/disable, note RAG, passkey step-up), rework the not-built-yet ranking (at-rest crypto, mavweb/mavcaldav tests, systemd), and add the rationale for deferring multi-user. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 KiB
Maven — current state (2026-07-03)
Consolidated status. The reactive↔proactive core is closed and testable through the web PWA. The SPEC's open items 1–7 are now 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. Big untouched infra remaining: at-rest security (sqlcipher) and systemd/ container deployment (still scripts + tmux). ~13.1k LOC + ~5.3k test, 202 tests.
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) +reminderpersist 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) →notestable.query→ embed → brute-force cosine top-k → confidence-gated (belowqueryMinScore0.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). - 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/signalevery 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, writescalendar_busy+ event facts through CoreAPI (value-change only). The loop gate already consumescalendar_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 frominternal/voice/wire.goso third-party clients don't need the Go source. - Passkey step-up (SPEC item 7):
internal/webauthndoes real WebAuthn — ES256/P-256 register + assert, ecdsa signature verification, rpIdHash + UP/UV flag binding (UV = the gesture), sign-count regression check.PasskeySessionbumps 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 (
Pdeathsigkills the child on any mavend death);kill-maven.shreaps strays (matches the model, not a bogusllama-server.*mavenpattern);start-maven.shwires-core+ poller.
Wired but needs a deploy action (not code)
desk_active(strongest presence signal) —scripts/desk-active.shruns 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 tomavpoll -kuma-key.
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-desktimer must be installed on the desk PC (not homesrv).
- a hypridle-gated
- 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.embedderfor 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_handshakeis OFF and should stay off — in this topology the phone only runs wg when outside, so a fresh handshake means AWAY, not here. Themavpoll -wgflag 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.
Not built yet (ranked by ROI)
- At-rest encryption — sqlcipher + cold-start unlock. Today the store is plain sqlite: disk access = full read. Passkey step-up now gates actions, but not the data at rest.
cmd/mavweb/cmd/mavcaldavtests — both are untested (0 test files). mavweb now carries the passkey + tools HTTP flow (the webauthn crypto is tested; the handlers and browser JS are not). Highest-value test gap.- Deployment — still
start-maven.sh+ tmux. systemd user units (one per binary, socket-activated,After=mavend.socket,Restart=on-failure) are specced, not written. Docker/Podman is a later phase. - Multi-user (SPEC item 8) — deliberately deferred, see the tail.
Housekeeping: go.mod isn't tidied — onnxruntime_go and coder/websocket
are used but marked // indirect. Passkey credentials are mavweb in-memory
(re-enroll after a mavweb restart — acceptable for single-user, by design).
Done since last revision: 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
proposedtool (maven suggests). A human enables it (fills argv- destructive) on the authed
mavweb /toolspage — never voice — and can disable it back toproposed(kept in the store, won't run).EnableTool/DisableToolsit atAuthStepUp; the gate is now live viaPasskeySession, so /tools enable requires a passkey assertion at/auth/passkeyfirst.
- destructive) on the authed
- Confirm turn: a destructive enabled tool replies "выполнить X? да/нет" and parks; the next utterance (ru/en yes-no) confirms or cancels (90s TTL).
- Config:
voice.toolsseeds 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 (
tokenizeiterated bytes, kept onlya-z0-9) → every RU utterance embedded to the zero vector → cosine 0 across all intents → misrouted toact(alphabetical tie-break). Now rune-based (unicode.IsLetter). This was the real cause of "Найди заметку" (a query) landing innotes; added note-retrieval query seeds too. - Notes are now browsable on
/dash—RecentNotesplumbed through the store + CoreAPI; voice-captured notes were previously only reachable via semanticquery. Earlier: notes/query recall,/dashmonitoring,wg_handshakepoller (NO-OP).
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/pttvoice,/api/signalpresence ingest,/api/ntfyWS-subscribe config,/dashread-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(ormavend.jsonin repo root).
Key files
cmd/mavend/{main,tick,voice}.go— daemon wiring, loop driver, voice handlerinternal/loop/{loop,rules,gather,feedback}.go— proactive engineinternal/store/— append-only facts/reminders/nudges/presence/notescmd/mavweb/{main.go,dash.html}— PWA bridge +/dashmonitoringinternal/router/{classifier,slots,stage0}.go— reactive routing + slot parseinternal/delivery/— dispatcher + ntfy/telegram/voice sinksinternal/auth/— scope/gate/policy;FloorEnrollment(same-uid = device trust) +webauthn.PasskeySession(real step-up for L3)internal/webauthn/,cmd/mavweb/webauthn.go— passkey register/assertcmd/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_idlater 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 PHASEin SPEC.md) so an autonomous agent doesn't adduser_idcolumns while touching the store and commit us to a schema before the constraints that shape it exist.