Merge pull request 'dialogue.Slots and router.Slots are hand-kept copies that already drifted' (#88) from task/365-dialogue-slots-and-router-slots-are-hand into master
This commit was merged in pull request #88.
This commit is contained in:
@@ -1,396 +0,0 @@
|
|||||||
beyond the model and tts work, the useful additions are mostly around **reliability, context, and reach**, not more intelligence.
|
|
||||||
|
|
||||||
## highest-value additions
|
|
||||||
|
|
||||||
### 1. unified event intake
|
|
||||||
|
|
||||||
maven should receive normalized events from:
|
|
||||||
|
|
||||||
* praxis
|
|
||||||
* calendar
|
|
||||||
* telegram
|
|
||||||
* local notifications
|
|
||||||
* system/service health
|
|
||||||
* manual checklists
|
|
||||||
* eventually email bridges
|
|
||||||
|
|
||||||
one internal envelope:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Event struct {
|
|
||||||
Source string
|
|
||||||
Kind string
|
|
||||||
EntityIDs []string
|
|
||||||
Title string
|
|
||||||
Body string
|
|
||||||
Priority string
|
|
||||||
OccurredAt time.Time
|
|
||||||
Payload json.RawMessage
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
this gives digestion one stable input instead of source-specific logic.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. explicit morning routine engine — **core engine done (2026-07-20)**
|
|
||||||
|
|
||||||
`internal/morning` — pure checklist engine, mirrors `internal/loop`/
|
|
||||||
`internal/routine`'s no-I/O contract. `Evaluate(routine, facts, now)` answers
|
|
||||||
"what's still missing" any time (order-independent — checks facts, not
|
|
||||||
sequence); `Due(routines, facts, last, now)` fires the once-per-day nag only
|
|
||||||
at `NudgeAt` (defaults to window end) and only when something's unevidenced,
|
|
||||||
with a `last`-map dedupe identical in shape to `routine.Due`'s cold-start/
|
|
||||||
last-fire tracking. Evidence is just a fact timestamped inside today's
|
|
||||||
window — manual (voice-tapped) and inferred (another daemon writing the same
|
|
||||||
key) are indistinguishable, satisfying the manual/inferred requirement for
|
|
||||||
free. Weekday/weekend variants are two `Routine`s with different `Weekdays`
|
|
||||||
sets under different names. Wired into `config.MorningRoutineConfig` +
|
|
||||||
`cmd/mavend/tick.go`'s `fireMorningRoutines` (reads only the fact keys the
|
|
||||||
configured items reference, dispatches through the normal severity/presence
|
|
||||||
routing table, body is literal joined item labels — not LLM-phrased, same
|
|
||||||
no-hallucination rationale as cron routines). 13 unit tests in
|
|
||||||
`internal/morning/morning_test.go`.
|
|
||||||
|
|
||||||
Added since (2026-07-20, same day): a read-only `/morning` page in mavweb —
|
|
||||||
`ipc.CoreAPI.MorningStatus` (new wire method, mirrors `TickTrace`'s
|
|
||||||
daemon-cache-only shape: the store adapter errors, `daemonAPI` serves it from
|
|
||||||
a `tickLoop.morningStatus` closure) returns each routine's active/window/
|
|
||||||
per-item done state, server-rendered same as `/trace` (no live-update loop —
|
|
||||||
checklist state moves on minutes, not seconds).
|
|
||||||
|
|
||||||
Not yet done: no config wired in `deploy/mavend.json` (no morning routines
|
|
||||||
configured on homesrv yet — add items there when the medicine/water/pets
|
|
||||||
fact keys the phone/desktop write are settled), no voice query path for
|
|
||||||
"what did I miss this morning" (Evaluate supports it; nothing calls it yet),
|
|
||||||
no way to create/edit routines from the web UI — construction still means
|
|
||||||
hand-editing config, deliberately deferred: routines are operator-declared
|
|
||||||
config (like cron routines), and a CRUD editor would mean moving them to a
|
|
||||||
DB table + hot-reload, a bigger change than this pass.
|
|
||||||
|
|
||||||
not ordinary reminders.
|
|
||||||
|
|
||||||
support:
|
|
||||||
|
|
||||||
* required morning items
|
|
||||||
* order-independent completion
|
|
||||||
* soft time windows
|
|
||||||
* skipped-step detection
|
|
||||||
* one nudge, not repeated spam
|
|
||||||
* manual and inferred completion evidence
|
|
||||||
* weekend/weekday variants
|
|
||||||
|
|
||||||
example:
|
|
||||||
|
|
||||||
```text
|
|
||||||
08:00–11:00
|
|
||||||
- medicine
|
|
||||||
- water
|
|
||||||
- pets
|
|
||||||
- check praxis attention
|
|
||||||
```
|
|
||||||
|
|
||||||
maven should know what is still missing, not merely fire four timers.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. cross-device presence
|
|
||||||
|
|
||||||
**status (2026-07-20):** the hysteresis engine and 3 of the listed signals are
|
|
||||||
already built and wired live: `internal/store/presence.go` (noisy-OR combiner
|
|
||||||
+ Schmitt-trigger bucket resolve), fed by `desk_active` (workstation, via
|
|
||||||
`scripts/desk-active.sh` posting to `/api/signal`), `page_heartbeat` (mavweb
|
|
||||||
tab, `app.js`), and `wg_handshake` (`mavpoll` polling `wg show`) — threaded
|
|
||||||
into the tick loop via `internal/loop/gather.go`. Not done: phone-reachable,
|
|
||||||
homesrv-available, audio-output, and active-maven-client signals from the
|
|
||||||
list below are still missing.
|
|
||||||
|
|
||||||
a small presence daemon on each trusted device:
|
|
||||||
|
|
||||||
* workstation active/idle
|
|
||||||
* phone reachable
|
|
||||||
* homesrv available
|
|
||||||
* last keyboard/mouse activity
|
|
||||||
* wireguard presence
|
|
||||||
* current audio output
|
|
||||||
* active maven client
|
|
||||||
|
|
||||||
mavend receives only compact state, not raw activity logs.
|
|
||||||
|
|
||||||
useful for:
|
|
||||||
|
|
||||||
* choosing delivery channel
|
|
||||||
* suppressing voice while away
|
|
||||||
* surfacing reminders when you return
|
|
||||||
* knowing whether an agent result should be spoken or sent as text
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. interruption policy — **done (2026-07-20), turned out to already be built**
|
|
||||||
|
|
||||||
audited the existing code before writing anything new: `internal/loop.Gate`
|
|
||||||
already answers deliver_now vs. drop (quiet-hours/cooldown/snooze/presence/
|
|
||||||
calendar-busy), and `cmd/mavend/tick.go`'s `digestQ` + `config.DigestConfig`
|
|
||||||
already implement queue/digest (low-severity nudges batch into one
|
|
||||||
notification, flushed on window elapsed or max-items reached). The four
|
|
||||||
outcomes below were already covered by these two mechanisms; nothing new to
|
|
||||||
build for the core policy.
|
|
||||||
|
|
||||||
Gap that *was* real: `deploy/mavend.json` had no `digest` block, so batching
|
|
||||||
was disabled in prod despite being fully implemented. Fixed — see the config
|
|
||||||
change alongside this note.
|
|
||||||
|
|
||||||
before delivering anything, evaluate:
|
|
||||||
|
|
||||||
```text
|
|
||||||
urgency
|
|
||||||
current activity
|
|
||||||
quiet hours
|
|
||||||
recent nudges
|
|
||||||
available channels
|
|
||||||
whether already surfaced
|
|
||||||
```
|
|
||||||
|
|
||||||
result:
|
|
||||||
|
|
||||||
```text
|
|
||||||
deliver_now
|
|
||||||
queue
|
|
||||||
digest
|
|
||||||
drop
|
|
||||||
```
|
|
||||||
|
|
||||||
this prevents maven from becoming annoying once praxis and other sources start producing more data.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. entity-aware memory — **done (2026-07-20)**
|
|
||||||
|
|
||||||
`03fa52d`/`9876187` (Vikunja #279): facts gain `Subject`/`EntityID`/
|
|
||||||
`ResolutionState`; an async enrichment worker resolves free-text subjects to
|
|
||||||
canonical Nexus entity_ids (mirrors Praxis's enrichment pattern). Ambiguous
|
|
||||||
or unreachable Nexus never guesses — the fact stays `pending` or terminal
|
|
||||||
`ambiguous`. Voice-tapped facts (`IntentFact`) now flow into the enrichment
|
|
||||||
queue automatically via an optional `Subject` field on `WriteFactReq` (old
|
|
||||||
callers unaffected).
|
|
||||||
|
|
||||||
Landed alongside this in the same session (not originally on this list, but
|
|
||||||
closes the plumbing gaps the last brief flagged for Nexus/Praxis maturity):
|
|
||||||
a typed Praxis lifecycle client (`398997f` — surface/acknowledge/resolve/
|
|
||||||
ignore/pin; fixes the surfaced≠acknowledged gap where reading an item aloud
|
|
||||||
left no trace), correlation-ID/version headers on the Nexus/Praxis clients
|
|
||||||
(`b743860`), entity-scoped Praxis attention queries (`0579ef9`), a durable
|
|
||||||
delivery outbox with begin-before-send/complete-after semantics
|
|
||||||
(`29f23e3`+`9ff726e` — closes a duplicate-send-on-crash bug), fail-closed
|
|
||||||
handling on ambiguous IPC mutation outcomes and Nexus/Hexis dependency
|
|
||||||
errors (`838fde1`+`d9fa4d6`), and a reusable fake-ecosystem test harness
|
|
||||||
with fault injection (`c932cd8`).
|
|
||||||
|
|
||||||
connect maven memory to nexus ids.
|
|
||||||
|
|
||||||
instead of:
|
|
||||||
|
|
||||||
```text
|
|
||||||
key = "кошачий фонтан"
|
|
||||||
```
|
|
||||||
|
|
||||||
store:
|
|
||||||
|
|
||||||
```text
|
|
||||||
entity_id = ent_pet_water_fountain
|
|
||||||
predicate = refilled_at
|
|
||||||
value = 2026-07-19T...
|
|
||||||
```
|
|
||||||
|
|
||||||
benefits:
|
|
||||||
|
|
||||||
* stable russian/english aliases
|
|
||||||
* fewer duplicate facts
|
|
||||||
* better “when did i last…” queries
|
|
||||||
* easier routine detection
|
|
||||||
* cleaner praxis correlation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. bounded follow-up state
|
|
||||||
|
|
||||||
for short continuations:
|
|
||||||
|
|
||||||
* “yes”
|
|
||||||
* “tomorrow”
|
|
||||||
* “the second one”
|
|
||||||
* “not that project”
|
|
||||||
* “do it later”
|
|
||||||
|
|
||||||
store explicit pending state instead of relying on chat history:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type PendingInteraction struct {
|
|
||||||
Kind string
|
|
||||||
Candidates []string
|
|
||||||
Args json.RawMessage
|
|
||||||
ExpiresAt time.Time
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
this matters a lot for a 1.7b model.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. evaluation lab — **skipped for now (2026-07-20)**
|
|
||||||
|
|
||||||
runs on a different machine (GPU box), and CPT is currently in progress
|
|
||||||
there — deprioritized until the training pipeline has a checkpoint to gate.
|
|
||||||
Not abandoned, just off the immediate list.
|
|
||||||
|
|
||||||
before every new checkpoint or lora deploy:
|
|
||||||
|
|
||||||
* routing accuracy
|
|
||||||
* slot accuracy
|
|
||||||
* malformed json rate
|
|
||||||
* russian/english mixed input
|
|
||||||
* ambiguous entity handling
|
|
||||||
* reminder vs note vs fact
|
|
||||||
* direct answer vs tool call
|
|
||||||
* confirmation safety
|
|
||||||
* phrasing quality
|
|
||||||
* latency and ram
|
|
||||||
|
|
||||||
also replay real anonymized traces against old and new checkpoints.
|
|
||||||
|
|
||||||
this should be a hard deployment gate.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8. replayable full-system simulator
|
|
||||||
|
|
||||||
fake:
|
|
||||||
|
|
||||||
* clock
|
|
||||||
* presence
|
|
||||||
* caldav
|
|
||||||
* telegram
|
|
||||||
* praxis
|
|
||||||
* nexus
|
|
||||||
* hexis
|
|
||||||
* stt
|
|
||||||
* tts
|
|
||||||
* llama-server
|
|
||||||
|
|
||||||
scenario:
|
|
||||||
|
|
||||||
```text
|
|
||||||
08:30 user appears
|
|
||||||
08:35 medicine not completed
|
|
||||||
08:40 correx agent waits
|
|
||||||
08:45 calendar sync stale
|
|
||||||
08:50 user says “what did i miss?”
|
|
||||||
```
|
|
||||||
|
|
||||||
assert:
|
|
||||||
|
|
||||||
* what tools were called
|
|
||||||
* what was surfaced
|
|
||||||
* what stayed unresolved
|
|
||||||
* what maven said
|
|
||||||
* what was not executed
|
|
||||||
|
|
||||||
this will save more time than another feature daemon.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## useful second-wave additions
|
|
||||||
|
|
||||||
### voice session quality
|
|
||||||
|
|
||||||
* barge-in
|
|
||||||
* interrupt tts on wake word
|
|
||||||
* partial stt display
|
|
||||||
* confidence-aware clarification
|
|
||||||
* retry only failed stt segment
|
|
||||||
* per-room microphone profiles
|
|
||||||
* noise-floor calibration
|
|
||||||
* short response mode when speaking
|
|
||||||
|
|
||||||
### notification bridge framework
|
|
||||||
|
|
||||||
small adapters for:
|
|
||||||
|
|
||||||
* ntfy
|
|
||||||
* telegram
|
|
||||||
* matrix
|
|
||||||
* web push
|
|
||||||
* android notification forwarding
|
|
||||||
* local dbus notifications
|
|
||||||
|
|
||||||
normalize into maven/praxis events instead of treating each as a separate feature.
|
|
||||||
|
|
||||||
### local knowledge ingestion
|
|
||||||
|
|
||||||
* markdown/docs ingestion
|
|
||||||
* git repo summaries
|
|
||||||
* project decision records
|
|
||||||
* conversation exports
|
|
||||||
* provenance and source links
|
|
||||||
* incremental reindexing
|
|
||||||
|
|
||||||
keep this read-only and separate from personal fact memory.
|
|
||||||
|
|
||||||
### service self-diagnostics
|
|
||||||
|
|
||||||
`maven doctor`:
|
|
||||||
|
|
||||||
* socket reachability
|
|
||||||
* model health
|
|
||||||
* stt/tts readiness
|
|
||||||
* embedder availability
|
|
||||||
* caldav freshness
|
|
||||||
* telegram poll state
|
|
||||||
* praxis/nexus/hexis reachability
|
|
||||||
* db integrity
|
|
||||||
* disk usage
|
|
||||||
* recent failures
|
|
||||||
|
|
||||||
### config and secret management
|
|
||||||
|
|
||||||
* schema-validated config
|
|
||||||
* config migration
|
|
||||||
* secret references instead of inline values
|
|
||||||
* dry-run validation
|
|
||||||
* redacted config dump
|
|
||||||
* per-daemon health config
|
|
||||||
* startup dependency report
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## things i would not build yet
|
|
||||||
|
|
||||||
* autonomous multi-step planning
|
|
||||||
* large external reasoner
|
|
||||||
* generic workflow engine
|
|
||||||
* self-editing memory
|
|
||||||
* automatic hexis actions from praxis
|
|
||||||
* emotion simulation beyond phrasing
|
|
||||||
* full home-assistant replacement
|
|
||||||
* more model layers before routing is stable
|
|
||||||
|
|
||||||
## recommended order
|
|
||||||
|
|
||||||
**status as of 2026-07-20:**
|
|
||||||
|
|
||||||
1. ~~evaluation lab~~ — **skipped, GPU-box work, deprioritized while CPT is in progress**
|
|
||||||
2. ~~entity-aware memory~~ — **done** (`03fa52d`/`9876187`, plus adjacent
|
|
||||||
Nexus/Praxis plumbing hardening — see item 5 above)
|
|
||||||
3. ~~morning routine engine~~ — **core engine done** (`internal/morning` +
|
|
||||||
`cmd/mavend` wiring — see item 2 above; not yet configured on homesrv,
|
|
||||||
no voice query, no web UI)
|
|
||||||
4. interruption/delivery policy
|
|
||||||
5. presence agents
|
|
||||||
6. unified event intake
|
|
||||||
7. full-system simulator
|
|
||||||
8. notification bridges
|
|
||||||
9. knowledge ingestion
|
|
||||||
10. voice-session polish
|
|
||||||
|
|
||||||
the main goal should be: **maven reliably knows what is happening, knows what you meant, and chooses the least annoying correct response**. everything else can wait.
|
|
||||||
|
|
||||||
-468
@@ -1,468 +0,0 @@
|
|||||||
## Maven — current state (updated 2026-07-20)
|
|
||||||
|
|
||||||
### Session 2026-07-20 — ecosystem hardening + entity-aware facts
|
|
||||||
|
|
||||||
Ten commits, focused on closing the Nexus/Praxis integration gaps flagged
|
|
||||||
as "wired but immature" in the prior review, plus the entity-aware-memory
|
|
||||||
backlog item (`20-07-2026-BACKLOG.md` item 5).
|
|
||||||
|
|
||||||
- **Entity-aware fact resolution (Vikunja #279)** — facts gain
|
|
||||||
`Subject`/`EntityID`/`ResolutionState`; an async worker resolves
|
|
||||||
free-text subjects to canonical Nexus entity_ids (mirrors Praxis's own
|
|
||||||
enrichment pattern). Ambiguous/unreachable Nexus never guesses — stays
|
|
||||||
`pending` or terminal `ambiguous`. Voice-tapped facts (`IntentFact`) flow
|
|
||||||
into the queue automatically via an optional `Subject` field on
|
|
||||||
`WriteFactReq` (old callers unaffected, no signature break).
|
|
||||||
- **Typed Praxis lifecycle client (Vikunja #271)** — `GetItem`/`Search`/
|
|
||||||
`Surface`/`Acknowledge`/`Resolve`/`Ignore`/`Pin`, routed through new RU/EN
|
|
||||||
dialogue verbs. Fixes a real lifecycle-invariant bug: reading an
|
|
||||||
attention item aloud now calls `Surface` — previously the digest path
|
|
||||||
read items without recording that they'd been surfaced, so "Maven
|
|
||||||
mentioned it" was indistinguishable from "never came up."
|
|
||||||
- **Durable delivery outbox (Vikunja #270)** — `BeginDeliveryAttempt`
|
|
||||||
before `Send`, `CompleteDeliveryAttempt` after; a stale `pending` row
|
|
||||||
found at startup reconciles to `unknown` (never silently resent or
|
|
||||||
dropped — same rule as Hexis's execution-timeout handling). Closes a
|
|
||||||
crash-window duplicate-send bug. Wired into `DispatchNudge`,
|
|
||||||
`DispatchReminder`, `RepeatUnacked`; reconciliation runs once at boot
|
|
||||||
before the tick loop resumes.
|
|
||||||
- **Fail-closed IPC/dependency handling (Vikunja #269, #272/#273)** —
|
|
||||||
ambiguous mutation outcomes (frame sent, reply lost) no longer blindly
|
|
||||||
retry; Nexus/Hexis dependency errors fail closed instead of guessing.
|
|
||||||
- **Correlation IDs + version headers (Vikunja #273)** — the hand-rolled
|
|
||||||
Nexus/Praxis HTTP clients now send `X-Nexus-Version`/`X-Praxis-Version`
|
|
||||||
and thread the same correlation ID already generated in
|
|
||||||
`executeCapability` through the whole call chain, matching the Hexis
|
|
||||||
client's existing behavior.
|
|
||||||
- **Entity-scoped Praxis attention queries** — callers holding a resolved
|
|
||||||
entity_id can ask "what needs attention for this entity" directly
|
|
||||||
instead of filtering the unscoped list client-side.
|
|
||||||
- **Fake-ecosystem test harness with fault injection** — a reusable
|
|
||||||
`fakeServer` (Nexus/Praxis/Hexis fixtures, runtime-toggleable
|
|
||||||
`SetFault`, fake clock) replacing ad-hoc per-test `httptest` servers;
|
|
||||||
covers a gap that had zero test coverage (`handlePraxisAct`) and adds a
|
|
||||||
fault-then-recovery regression test for the fail-closed fixes above.
|
|
||||||
- **Ops fix** — `deploy/mavend.json`'s phraser was pointed at a 4B model
|
|
||||||
with `n_gpu_layers=99`, which OOM'd under memory pressure and left a
|
|
||||||
zombie `llama-server` child; swapped to the 2B Qwen model matching the
|
|
||||||
intended resident-model size.
|
|
||||||
|
|
||||||
Net effect: the Nexus/Praxis wiring described as "plumbing exists, thin
|
|
||||||
compared to Maven's test depth" in the prior review is now materially
|
|
||||||
hardened — typed clients, fail-closed error handling, durable delivery,
|
|
||||||
and a proper fault-injection test harness are all in place. Evaluation lab
|
|
||||||
(`20-07-2026-BACKLOG.md` item 7) is explicitly skipped for now — it runs
|
|
||||||
on the GPU box, which is occupied by CPT. Morning routine engine (backlog
|
|
||||||
item 3) is next up, not started.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **Resolved 2026-07-30 (task #318).** The resident checkpoint is
|
|
||||||
> **Qwen3.5-0.8B** (`Q4_K_M`), set in `deploy/mavend.json`; the **target** is
|
|
||||||
> the locally CPT'd **Qwen3-1.7B**, still training (#122). Older model claims
|
|
||||||
> below — the LFM references, the pipeline line, and the "swapped to the 2B
|
|
||||||
> Qwen model" ops entry above — are historical. Read them as a log of what was
|
|
||||||
> true at the time, not as current fact. Note also that `/mnt/hdd1/llms` is
|
|
||||||
> bind-mounted over `models/llm/`, so the LFM2.5 gguf in the repo tree is
|
|
||||||
> never loaded.
|
|
||||||
|
|
||||||
Architecture decision (as written on 2026-07-20): the target resident
|
|
||||||
router/phraser is the locally trained Qwen3-1.7B model — still the target as
|
|
||||||
of 2026-07-30. Older LFM references below describe the then-deployed
|
|
||||||
historical stack, not the target checkpoint. RU CPT has a successful
|
|
||||||
full-weight checkpoint at step 1000/8077; evaluation and Qwen3 SFT tooling are
|
|
||||||
tracked in `docs/plans/2026-07-18-qwen3-resident-training-eval.md`.
|
|
||||||
|
|
||||||
Consolidated status. The reactive↔proactive core is closed and testable through
|
|
||||||
the web PWA. The former SPEC's open items 1–7 (now `docs/design.md` § execution ledger) 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 →
|
|
||||||
resident phraser (llama-server subprocess; Qwen3.5-0.8B as of 2026-07-30 —
|
|
||||||
this line historically named "LFM 2.5-1.2B") → 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:<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 `docs/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.
|
|
||||||
- **`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.
|
|
||||||
- **Cold-start unlock tests are missing** — the key wrap/unwrap code
|
|
||||||
(`internal/webauthn/keywrap.go`) and locked-mode IPC gating (`cmd/mavend/main.go`)
|
|
||||||
are correct but have **zero test coverage**. The roadmap (item 2.1) required
|
|
||||||
three new test cases (wrap/unwrap round-trip, wrong-cred unwrap fails,
|
|
||||||
locked-mode IPC rejects non-unlock methods); none were written. `make test`
|
|
||||||
is green by omission. Write these before relying on the cold-start path with
|
|
||||||
real keys.
|
|
||||||
|
|
||||||
### Done since last revision (overnight-jul6, 2026-07-06)
|
|
||||||
|
|
||||||
Seven tasks (session board `SESSION-06-07-2026.md`, deleted 2026-07-30 — see git history), 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.
|
|
||||||
|
|
||||||
Since then (**2026-07-06, third session** — roadmap execution agent):
|
|
||||||
|
|
||||||
- **Cold-start unlock (ROADMAP 2.1)** — the at-rest AES key is now wrapped
|
|
||||||
(HKDF-SHA256 + AES-256-GCM, stdlib-only — no `x/crypto` dep) with the passkey
|
|
||||||
credential's public key and persisted to disk. At boot, if a wrapped key file
|
|
||||||
exists AND no env key is set, mavend starts **locked**: the IPC server runs
|
|
||||||
but `srv.Check` rejects everything except `MethodAssertStepUp` +
|
|
||||||
`MethodUnlock`. A passkey assertion at `/auth/passkey` calls `MethodUnlock`
|
|
||||||
with the credential's public key → unwraps the blob → opens the store → wires
|
|
||||||
voice/loop/delivery → `srv.SetAPI` swaps the locked stub for the real
|
|
||||||
CoreAPI. mavweb's `RegisterFinish` wraps the env key on enrollment;
|
|
||||||
`AssertFinish` calls `Unlock` on assertion. Env-key fallback preserved
|
|
||||||
(dev/CI path unchanged). **Test gap:** the roadmap required three new test
|
|
||||||
cases (wrap/unwrap round-trip, wrong-cred unwrap fails, locked-mode IPC
|
|
||||||
rejects non-unlock methods) — none were written. The code is correct but
|
|
||||||
untested; `make test` is green by omission, not coverage.
|
|
||||||
- **Conversation depth (ROADMAP 3.2)** — cross-intent anaphora + fact-by-key
|
|
||||||
lookup. `AnaphoraResolver` in `router/slots.go` detects RU pronouns
|
|
||||||
(это/он/она/оно/тот/мой + inflected forms). `followUpMerge` now handles
|
|
||||||
three cases: same-intent slot inheritance (existing), cross-intent anaphora
|
|
||||||
(Query/Fact/Reminder after a Fact with a pronoun inherits the prior key +
|
|
||||||
time), and query-after-fact (a query following a fact inherits the key for
|
|
||||||
fact-by-key lookup). `Session.History []Turn` added as the multi-turn
|
|
||||||
scaffold (capped at 4). 7 new test cases including the exact done-when
|
|
||||||
scenarios (anaphora query-after-fact, three-turn break, explicit-key-wins).
|
|
||||||
- **Routing quality + persona (ROADMAP 4.1/4.4)** — `QueryMinScore` is now a
|
|
||||||
config knob (`voice.query_min_score`, default 0.55) instead of a hardcoded
|
|
||||||
const. `make download-embedder` fetches Xenova/paraphrase-multilingual-
|
|
||||||
MiniLM-L12-v2 (~90MB ONNX) + tokenizer; AGENTS.md documents the embedder +
|
|
||||||
libonnxruntime setup. `Persona` field in `VoiceConfig` prepends to every
|
|
||||||
LLM system prompt (nudge phrasing, note queries, general knowledge); empty
|
|
||||||
= current hardcoded feminine-gendered Russian persona. Also fixed two
|
|
||||||
pre-existing data races found by `-race`: `voice/server.go` wg.Add vs
|
|
||||||
wg.Wait (accept mutex), `mavweb/server.go` s.api field (atomic.Value).
|
|
||||||
|
|
||||||
- **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`, deleted 2026-07-30 — see git history; 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. **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: 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 deeper now, still not full dialogue.** The router
|
|
||||||
classifies one utterance → one reply, but `internal/dialogue` carries
|
|
||||||
context across turns: a 2-min session inherits slots for same-intent
|
|
||||||
follow-ups («напомни завтра» → «…позвонить маме»), and cross-intent
|
|
||||||
anaphora («запиши что я пил воду» → «когда я это сделал?») now resolves
|
|
||||||
RU pronouns (это/он/она/оно/тот/мой + inflections) to the prior turn's
|
|
||||||
key for fact-by-key lookup. `Session.History []Turn` is the scaffold for
|
|
||||||
real multi-turn. Still missing: LLM-driven dialogue manager (decide
|
|
||||||
ask-vs-act), anaphora beyond RU pronouns, single-slot session (single-user
|
|
||||||
box). 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". `make download-embedder` now fetches the multilingual MiniLM
|
|
||||||
model + AGENTS.md documents libonnxruntime setup; `voice.query_min_score`
|
|
||||||
is a config knob (default 0.55) so the floor can be tuned without recompile.
|
|
||||||
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. Custom TTS voice (kami-picked, replaces the irina
|
|
||||||
floor — [[custom-voice-training]]) is still a future item.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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 `docs/design.md` § Users) 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.
|
|
||||||
+12
-5
@@ -12,13 +12,21 @@ import (
|
|||||||
// which waits on voice-print attribution (see PROGRESS multi-user deferral).
|
// which waits on voice-print attribution (see PROGRESS multi-user deferral).
|
||||||
const voiceDialogueID = "voice"
|
const voiceDialogueID = "voice"
|
||||||
|
|
||||||
// toDialogueSlots projects the router's slots onto the dialogue layer's subset
|
// toDialogueSlots and applyDialogueSlots are the only bridge between
|
||||||
// (everything except the fact Value, which the dialogue layer doesn't carry).
|
// router.Slots and dialogue.Slots. dialogue must not import router (import
|
||||||
|
// cycle), so the two structs are hand-kept copies and every field has to be
|
||||||
|
// carried by hand here. Adding a field to either struct without adding it to
|
||||||
|
// BOTH functions loses a slot silently — nothing fails to build. The tests in
|
||||||
|
// slotsparity_test.go fail when the field sets or the converters stop matching;
|
||||||
|
// when they do, fix these two functions, not the tests.
|
||||||
|
|
||||||
|
// toDialogueSlots projects the router's slots onto the dialogue layer's copy.
|
||||||
func toDialogueSlots(s router.Slots) dialogue.Slots {
|
func toDialogueSlots(s router.Slots) dialogue.Slots {
|
||||||
return dialogue.Slots{
|
return dialogue.Slots{
|
||||||
Time: s.Time,
|
Time: s.Time,
|
||||||
HasTime: s.HasTime,
|
HasTime: s.HasTime,
|
||||||
Key: s.Key,
|
Key: s.Key,
|
||||||
|
Value: s.Value,
|
||||||
HasKey: s.HasKey,
|
HasKey: s.HasKey,
|
||||||
Text: s.Text,
|
Text: s.Text,
|
||||||
Fn: s.Fn,
|
Fn: s.Fn,
|
||||||
@@ -27,11 +35,10 @@ func toDialogueSlots(s router.Slots) dialogue.Slots {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyDialogueSlots writes inherited dialogue slots back onto router slots,
|
// applyDialogueSlots writes dialogue slots back onto router slots.
|
||||||
// preserving router-only fields (Value) the dialogue layer never touched.
|
|
||||||
func applyDialogueSlots(base router.Slots, d dialogue.Slots) router.Slots {
|
func applyDialogueSlots(base router.Slots, d dialogue.Slots) router.Slots {
|
||||||
base.Time, base.HasTime = d.Time, d.HasTime
|
base.Time, base.HasTime = d.Time, d.HasTime
|
||||||
base.Key, base.HasKey = d.Key, d.HasKey
|
base.Key, base.Value, base.HasKey = d.Key, d.Value, d.HasKey
|
||||||
base.Text = d.Text
|
base.Text = d.Text
|
||||||
base.Fn, base.Args, base.HasFn = d.Fn, d.Args, d.HasFn
|
base.Fn, base.Args, base.HasFn = d.Fn, d.Args, d.HasFn
|
||||||
return base
|
return base
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// mavend/simulator_test.go — the replayable full-system simulator
|
// mavend/simulator_test.go — the replayable full-system simulator
|
||||||
// (Vikunja #284, 20-07-2026-BACKLOG.md item 7).
|
// (Vikunja #284).
|
||||||
//
|
//
|
||||||
// # What it is
|
// # What it is
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kami/maven/internal/dialogue"
|
||||||
|
"github.com/kami/maven/internal/router"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSlotsParity — dialogue.Slots is a hand-kept copy of router.Slots
|
||||||
|
// (dialogue must not import router: import cycle). Drift is silent, so this
|
||||||
|
// test compares the two field sets by name and type. If it fails, add the new
|
||||||
|
// field to both structs AND to toDialogueSlots/applyDialogueSlots in
|
||||||
|
// followup.go — do not relax the test.
|
||||||
|
func TestSlotsParity(t *testing.T) {
|
||||||
|
fields := func(v any) map[string]string {
|
||||||
|
rt := reflect.TypeOf(v)
|
||||||
|
out := make(map[string]string, rt.NumField())
|
||||||
|
for i := 0; i < rt.NumField(); i++ {
|
||||||
|
f := rt.Field(i)
|
||||||
|
out[f.Name] = f.Type.String()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
rf, df := fields(router.Slots{}), fields(dialogue.Slots{})
|
||||||
|
for name, typ := range rf {
|
||||||
|
dt, ok := df[name]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("router.Slots.%s (%s) missing from dialogue.Slots", name, typ)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if dt != typ {
|
||||||
|
t.Errorf("field %s: router has %s, dialogue has %s", name, typ, dt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name, typ := range df {
|
||||||
|
if _, ok := rf[name]; !ok {
|
||||||
|
t.Errorf("dialogue.Slots.%s (%s) missing from router.Slots", name, typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSlotsRoundTrip — the converters carry every field. A field the parity
|
||||||
|
// test accepts can still be dropped in transit, so round-trip a fully
|
||||||
|
// populated value and compare.
|
||||||
|
func TestSlotsRoundTrip(t *testing.T) {
|
||||||
|
full := router.Slots{
|
||||||
|
Time: time.Date(2026, 8, 2, 11, 0, 0, 0, time.UTC),
|
||||||
|
HasTime: true,
|
||||||
|
Fn: "restart",
|
||||||
|
Args: []string{"nginx"},
|
||||||
|
HasFn: true,
|
||||||
|
Key: "water",
|
||||||
|
Value: `"drank"`,
|
||||||
|
HasKey: true,
|
||||||
|
Text: "выпил воды",
|
||||||
|
}
|
||||||
|
// Every field must be non-zero, or the round-trip proves nothing.
|
||||||
|
rv := reflect.ValueOf(full)
|
||||||
|
for i := 0; i < rv.NumField(); i++ {
|
||||||
|
if rv.Field(i).IsZero() {
|
||||||
|
t.Fatalf("field %s is zero: extend this fixture so the round-trip covers it",
|
||||||
|
rv.Type().Field(i).Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := applyDialogueSlots(router.Slots{}, toDialogueSlots(full)); !reflect.DeepEqual(got, full) {
|
||||||
|
t.Errorf("round-trip lost a slot:\n got %+v\nwant %+v", got, full)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,48 @@
|
|||||||
# maven — feature ranking
|
# maven — feature ranking
|
||||||
|
|
||||||
|
> **Archived 2026-08-02 (V-447).** The mandatory and easy tiers are now Vikunja tasks
|
||||||
|
> 449-458. Two of those closed immediately, because the ranking was stale. Quiet hours
|
||||||
|
> (V-450) ship as `QuietHoursConfig` plus the care gate in `internal/loop/loop.go`.
|
||||||
|
> Schema migrations (V-451) ship as `internal/store/migrations.go` on `PRAGMA
|
||||||
|
> user_version`. The doable and epic tiers stay here because they are reasoning. Some of
|
||||||
|
> them exist only to record why something is not worth doing yet. Read this for the why,
|
||||||
|
> not as a work queue, and check the code before believing a gap.
|
||||||
|
|
||||||
> dated 2026-07-03. companion to `docs/design.md` (folded from the former `maven.md`). ranks everything discussed post-repo-state against the infra blockers, not a replacement for the build order.
|
> dated 2026-07-03. companion to `docs/design.md` (folded from the former `maven.md`). ranks everything discussed post-repo-state against the infra blockers, not a replacement for the build order.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## what already shipped (checked against the code, 2026-08-02)
|
||||||
|
|
||||||
|
One month old and already wrong in ten places. Everything below is marked
|
||||||
|
built after reading the code, not the board. Read the tiers underneath with this list in
|
||||||
|
hand.
|
||||||
|
|
||||||
|
Infra 1 and 2, the two the ranking says block every feature, are both done. sqlcipher
|
||||||
|
at-rest ships as `Store.enc` plus `OpenEncrypted`, a tmpfs working copy re-encrypted on
|
||||||
|
`Close`, keyed from `db_key_env` in `deploy/mavend.json`. mavweb and mavcaldav are no
|
||||||
|
longer at zero coverage: seven test files under `cmd/mavweb`, including
|
||||||
|
`credentials_test.go` and `passkey_prf_test.go`, and two under `cmd/mavcaldav`. Infra 3
|
||||||
|
is stale in the other direction. There are still no systemd units, but the deploy is
|
||||||
|
`deploy/ecosystem/docker-compose.yml`, not scripts and tmux.
|
||||||
|
|
||||||
|
Doable tier, built: rule trace and explanation as `/trace` plus `internal/loop/explain.go`.
|
||||||
|
Recurring reminders as the cron column, `NextFireTs` and `RescheduleReminder`
|
||||||
|
(`internal/store/reminders.go:206`), so "fires once right now" is wrong. Stale-reminder
|
||||||
|
burst collapse as `collapseReminders` (`internal/loop/gather.go:209`). Revert as
|
||||||
|
`VoidLatestFact` (`internal/store/facts.go:300`). Digest mode as `internal/store/digest.go`.
|
||||||
|
Testing infra as the simulator and the eval lab (V-284, V-278). Passkey persistence as the
|
||||||
|
JSON-backed `credentialStore` in `cmd/mavweb/credentials.go`. Most integrations shipped as
|
||||||
|
their own QA tasks (V-246 mail, V-256 smarthome, V-258 rss, V-259 crawler).
|
||||||
|
|
||||||
|
Doable tier, still open: correx, systemd units, memory decay and duplicate detection
|
||||||
|
(nothing in `internal/memory` touches it), backup automation, import and export, barge-in.
|
||||||
|
|
||||||
|
Epic tier, unbuilt as ranked. `event.Bus` exists (`cmd/mavend/intake.go:57`) but it is the
|
||||||
|
intake journal from V-283, not the rewrite of facts into projections that this tier means.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## infra — blocks everything below, in order
|
## infra — blocks everything below, in order
|
||||||
|
|
||||||
1. **sqlcipher at-rest** — auth chapter (cold-start = layer-3, key-in-core-only) is fiction without it. plain sqlite on disk right now, anyone with disk access reads everything.
|
1. **sqlcipher at-rest** — auth chapter (cold-start = layer-3, key-in-core-only) is fiction without it. plain sqlite on disk right now, anyone with disk access reads everything.
|
||||||
+2
-2
@@ -672,8 +672,8 @@ Broadening to home automation, media or comms is JSON, not code.
|
|||||||
|
|
||||||
## Execution ledger
|
## Execution ledger
|
||||||
|
|
||||||
Condensed from `ROADMAP.md` (2026-07-06). The live queue is
|
Condensed from `ROADMAP.md` (2026-07-06). The live queue is the Vikunja board
|
||||||
`20-07-2026-BACKLOG.md`; current state is `PROGRESS.md`.
|
(project Maven, ID 2); this table is history, not a work list.
|
||||||
|
|
||||||
| # | Item | Prio | Status |
|
| # | Item | Prio | Status |
|
||||||
|---|------|------|--------|
|
|---|------|------|--------|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
// 3. if no live session exists, Send returns voice.ErrNoSession
|
// 3. if no live session exists, Send returns voice.ErrNoSession
|
||||||
// (wrapped). The daemon logs the partial dispatch; an OPEN deferred
|
// (wrapped). The daemon logs the partial dispatch; an OPEN deferred
|
||||||
// question is whether the dispatcher should reroute to away-channels
|
// question is whether the dispatcher should reroute to away-channels
|
||||||
// instead of returning partial — listed in PROGRESS.md.
|
// instead of returning partial.
|
||||||
//
|
//
|
||||||
// Import direction: voicesink imports internal/tts (synth seam) and
|
// Import direction: voicesink imports internal/tts (synth seam) and
|
||||||
// internal/voice (Sessions registry). Both are siblings of delivery; the
|
// internal/voice (Sessions registry). Both are siblings of delivery; the
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// Package event is the unified intake envelope (Vikunja #283,
|
// Package event is the unified intake envelope (Vikunja #283).
|
||||||
// 20-07-2026-BACKLOG.md item 1).
|
|
||||||
//
|
//
|
||||||
// # The problem it solves
|
// # The problem it solves
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package morning is maven's morning routine engine — item #3 off the
|
// Package morning is maven's morning routine engine — item #3 off the
|
||||||
// 2026-07-20 backlog (see Maven/20-07-2026-BACKLOG.md).
|
// 2026-07-20 backlog (Vikunja #280).
|
||||||
//
|
//
|
||||||
// A Routine is NOT four independent reminder timers. It's a checklist for a
|
// A Routine is NOT four independent reminder timers. It's a checklist for a
|
||||||
// daily window: several Items, each evidenced by a fact key, completed in
|
// daily window: several Items, each evidenced by a fact key, completed in
|
||||||
|
|||||||
Reference in New Issue
Block a user