Commit Graph

28 Commits

Author SHA1 Message Date
kami 784d688b44 Merge the clarify wiring
# Conflicts:
#	internal/dialogue/clarify.go
#	internal/dialogue/clarify_test.go
2026-07-31 02:30:59 +04:00
kami cfd38d53cf Ask the question, then act on the answer
On a clarify decision with one identifiable gap she now asks instead of saying
"не поняла", and parks the request. The next utterance is parsed as the answer
with the router's own extractor and the completed decision runs through
applyAction like any other — so a clarified act still needs the allowlist and
still hits the destructive confirm gate. An answer that does not fill the gap
drops the request; she never asks twice. Also pulls the session-store block
that HandlePushToTalk and handleText both had into rememberTurn, since the
clarify path needed a third copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:26:11 +04:00
kami bf99fd4192 Add a voice.llm_router flag, default off
Wires cmd/mavend/voice.go to build the LLM router when the operator asks
for it. Default false, so nothing changes on the deploy box.
Look at pickLLMRouter: the flag on with no llama-server logs one line and
keeps the classifier, it never fails a turn.
The default stays off until the router can refuse (#359) and the extractor
runs on LLM decisions — both noted as TODOs in config.go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:15:30 +04:00
kami 9876187721 Wire voice-tapped facts into entity resolution; fix phraser model config
WriteFactReq gains an optional Subject field (empty = old behavior,
no CoreAPI signature change) and the IntentFact handler now passes the
fact's key as its resolution subject, so voice-tapped facts flow into
the Vikunja #279 enrichment queue automatically.

Also: 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, keeping GPU offload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
2026-07-20 12:13:30 +04:00
kami 398997f5c1 Add typed Praxis lifecycle client + acknowledge/resolve/ignore/pin/surface verbs
Maven previously only called Praxis list_attention/list_changes and read
untyped maps. Adds a typed praxisItem struct plus GetItem/Search/Surface/
Acknowledge/Resolve/Ignore/Pin client methods, and routes new dialogue
verbs (RU + EN aliases) through handlePraxisAct to each.

Also fixes a lifecycle-invariant bug: reading attention items aloud now
calls Surface, not nothing — per ECOSYSTEM-SPEC.md §2.3 surfaced !=
acknowledged, and previously the digest path didn't record surfacing at
all, so 'Maven mentioned it' left no trace distinguishable from 'never
came up'.

Vikunja #271.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
2026-07-20 03:05:41 +04:00
kami d9fa4d6613 Fail closed on Nexus/Hexis dependency errors, accept flat resolve shape
Vikunja #268 (P0): handleHexisAct swallowed genuine Nexus resolve errors
and Hexis capability-discovery errors into "" or an empty capability list,
which fell through to the local system command executor — a dependency
outage silently looked identical to "not an ecosystem entity" or "no
capabilities registered", violating the spec's degrade-independently /
never-silent-all-clear invariant.

- resolveEntityReference's error is now distinguished from a legitimate
  not_found: only the latter falls through.
- discoverCapabilities now returns (caps, err) instead of collapsing a
  Hexis failure into an empty slice; a real error stops the action with
  a degraded-mode spoken reply instead of reaching h.tools.Exec.
- nexusResolveResult gains a custom UnmarshalJSON to accept the flat
  entity_id/entity_type/display_name shape from ECOSYSTEM-SPEC.md §1.5
  (Nexus now emits both shapes; Maven now reads both).
- Added regression tests: flat-shape resolve, Nexus error fails closed,
  Hexis error fails closed, not_found still falls through to local exec.
2026-07-20 01:08:11 +04:00
kami 6c92f85d10 feat(ecosystem): compliant Praxis/Hexis integration + vendored build
Bring the Nexus/Praxis/Hexis integration in line with
MAVEN_ECOSYSTEM_ARCHITECTURE.md:

- Praxis over HTTP: drop the in-process praxis.db open (praxisstore/
  praxistools) and call praxisd's /api/v1/tools/* API via a new praxisClient.
  Honors the "no component reads another's DB" invariant (AC#12).
  PraxisConfig.DBPath -> URL.
- Hexis confirmation gate: mutating capabilities (ReadOnly=false) now park a
  bound pendingHexis confirmation and require a spoken "да" before executing;
  read-only run immediately (AC#7, no auto attention->action).
- Capability safety: >1 verb match is ambiguous -> ask instead of firing the
  first; ambiguous Nexus resolution asks for clarification (AC#2).
- Correlation IDs on Hexis execute, recorded in the cross-service trace.
- Bug: importance arrives as JSON float64 over HTTP, not int.
- Tests: confirm-gate, decline, read-only, and ambiguity paths.

Build: vendor/ bakes in the hexis client (replace-directed at a sibling repo
outside the Docker context); Dockerfile builds from vendor and no longer
`go mod download`s the unreachable replace paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:24:33 +04:00
kami 6a5121657a feat: {response,mood} output contract + router removal, TTS piper plan
Daemon side of Decision B: parse {"response","mood"} across the 4 consumers
(replier, nudges, reminders, chat), fall back to legacy formats. Drop the
LLM router — the classifier handles routing; replier/phraser share one
llm.Client (timeout 20s->60s). llm.Client reads reasoning_content when
content is empty (thinking models).

Docs: TTS piper-student plan (OmniVoice teacher -> piper student, from
scratch, phoneme-first). CLAUDE.md training guide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 22:51:50 +04:00
kami c0c11cd36d feat: LLM phraser, shared LLM client, and LLM replier
- Add llmphraser: LFM-based phraser implementing Phraser interface with
  PhraseChat, PhraseNudge, PhraseReactive, and PhraseReminder methods.
- Add shared internal/llm/client: llama-server completion client used by
  both the phraser (talking back) and router (routing), sharing one model.
- Add LLMReplier in mavend: replaces StubReplier for chat/nudge/reactive
  replies, falls back to stub on model errors.
- Update Phraser interface: add PhraseChat method, update stub to match.
- Wire LLM phaser into mavend voice init, plumb LLM config from JSON.
2026-07-10 15:48:55 +04:00
kami d493be34b2 dateparser: python shell-out with two-step parse + russian qualifier pre-processing
PythonDateParser shells out to python3 with the dateparser library for
full natural-language date/time extraction. Two-step approach:
1. search_dates() finds the date substring in surrounding text
2. parse() re-parses the substring for correct time resolution

Russian time qualifiers (утра/вечера/дня/ночи) are pre-processed to
AM/PM before parsing — dateparser drops them during substring extraction.

Falls back to StubDateTimeParser when python3 or dateparser isn't
available (graceful degradation, no hard runtime dependency).

Dockerfile updated: python3 + dateparser==1.4.1 in runtime stage.
2026-07-06 19:09:09 +04:00
kami bf4009ca4f voice/seeds: track seed files, add russian time parser, guard replySystem
five fixes spotted during routing investigation:

- gitignore: replace blanket models/ ignore with per-dir exceptions
  (/models/embedder/, /models/stt/, /models/tts/) so the seed text
  files under models/seeds/ are tracked in version control
- query.txt: fix merged line — 'сколько стоит свет в этом месяце' and
  'найди заметку про сервер' were fused with no separator
- reminder.txt: add 11 pure-verb reminder seeds without time expressions
  to shift centroid toward the reminding intent rather than time-lexicon
- StubDateTimeParser: add Russian 'через <N> <unit>'/'через час'/'через
  полчаса', 'сегодня'/'завтра'/'послезавтра' with optional clock, and
  'в <clock>' scan. Add Russian word numbers (один-десять) and unit
  inflections (час/часа/часов, минута/минуты/минут, день/дня/дней,
  неделя/недели/недель). Also adds missing English day/week units.
- replySystem: guard time branch against duration queries ('сколько
  времени прошло') reaching it via the classifier path after the
  stage-0 grammar's build filter rejects them. Mirrors stage0.go
  duration keywords.
2026-07-06 18:38:48 +04:00
kami ce3d8e65f2 voice/routing: fix time-query misroute (seed collision, threshold, stage-0 grammars)
three bugs causing time queries to land on reminder or fact intent:

- seed collision: query.txt and system.txt shared identical time/date
  seeds (который час, сколько времени), making system intent
  indistinguishable from query intent in centroid space
- threshold (0.35) too low for ONNX embedder — cosine similarities
  cluster 0.5-0.7 for related intents, so Clarify never fired
- reminder centroid contaminated by time-lexicon (every seed has a time
  expression), pulling any time-word utterance toward reminder intent

fixes:
- remove 3 duplicate time/date seeds from query.txt (keep in system.txt)
- DefaultRouterThreshold 0.35 -> 0.55
- stage-0 grammar for напомни/remind me -> IntentReminder, bypasses
  classifier (fixes 'напомни через час' being misrouted to fact)
- stage-0 grammars for time/date system queries (сколько времени,
  который час, какой сегодня день) -> IntentSystem, with Build filter
  to exclude elapsed/duration queries (сколько времени прошло)
- time parser fallback in applyAction for stage-0 reminder matches
  (extractor doesn't run on stage-0 decisions)
2026-07-06 18:23:13 +04:00
kami 05236ad480 3.2 conversation depth: cross-intent anaphora + fact-by-key query
- session.go: add History []Turn + Turn type for multi-turn context
- slots.go: add AnaphoraResolver with Resolve() for RU pronoun detection
  (это/он/она/оно/тот/мой and inflected forms)
- followup.go: extend followUpMerge with cross-intent inheritance:
  Query/Fact/Reminder after a Fact with anaphora inherits the key.
  Same-intent path unchanged. Anaphora detection from utterance.
- voice.go: add fact-by-key lookup path in applyAction for IntentQuery
  when dialogue resolved an anaphoric reference (calls LatestFact,
  formats with formatTime helper). History tracked in Session.History
  capped at 4 most recent turns.
- followup_test.go: 7 new test cases: anaphora query-after-fact,
  no-inheritance-without-anaphora, three-turn break, anaphora in
  reminder, anaphora in fact, explicit key wins, time inheritance.
  make test green (303+, -race, all 29 packages).
2026-07-06 13:40:16 +04:00
kami b7eb53a3b4 4.1 routing quality + 4.4 persona prompt
- VoiceConfig: add QueryMinScore (default 0.55) + Persona config fields
- voice.go: remove queryMinScore const, wire from cfg.Voice.QueryMinScore
  as reactiveHandler field
- llmphraser.go: add Persona to Config, prepend to system prompts in
  chat and query paths (systemPrompt/querySystemPrompt methods)
- main.go: pass personaFromCfg into both phraser config blocks
- Makefile: add download-embedder target (Xenova/paraphrase-multilingual-
  MiniLM-L12-v2, ~90MB ONNX)
- AGENTS.md: document embedder model download + libonnxruntime setup
- server.go: fix pre-existing wg.Add vs wg.Wait data race using accept
  mutex. make test green, zero races across all 29 packages.
2026-07-06 13:35:39 +04:00
kami 59a4e06615 maven: scheduled routines + persistent long-term memory
Two additive proactive/recall features.

Routines (internal/routine): a third proactive class beside reminders
(user-stated) and care rules (world-state) — operator-declared clockwork.
config.routines[] (cron + literal RU body + severity) fire through the
normal dispatcher on schedule. Bodies are literal, not LLM-phrased (can't
hallucinate); rule name routine:<name> keeps them out of the care
autotuner; a cold-start guard seeds on first sight so a restart never
replays a missed schedule. Pure routine.Due + config validation, unit-
tested; the tick driver holds the last-fired map and calls fireRoutines.

Persistent memory (internal/store/memory.go): store.MemoryStore backs the
memory.Store interface with the SAME encrypted sqlite db — survives
restarts and recall text inherits at-rest encryption (no plaintext
sidecar). float32-blob vectors, brute-force cosine (ANN is a later swap
behind the interface), upsert-by-id. The daemon wires st.VectorMemory()
into wireVoice; the in-memory impl stays the test/no-store floor. Closes
the "in-memory only, lost on restart" gap (PROGRESS #8).

Gate green: gofmt/vet clean, -race across routine/config/store/mavend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2PNdwDj2Gt8YW294J7oSc
2026-07-06 12:37:28 +04:00
kami 388d4257ee maven: wire dialogue slot carry-over into the voice path (task 6)
The dialogue library (internal/dialogue, task 6) shipped tested but unwired.
Wire it: reactiveHandler now holds a 2-min SessionStore, and each turn fills
its missing slots from a prior same-intent, non-expired turn via InheritSlots
before acting, then records itself for the next follow-up. Single-user box →
one session slot (voiceDialogueID).

Guardrails (followUpMerge, unit-tested): only same-intent turns inherit (a new
intent is a fresh command); clarify turns and expired/nil priors never inherit;
InheritSlots fills gaps only, so a fully-slotted turn is untouched; the fact
Value (router-only) survives the dialogue.Slots round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 12:15:17 +04:00
kami cda614d1bb maven: fix нью-йорк transliteration in weather location map 2026-07-06 11:01:49 +04:00
kami d52f60c54e maven: fix test mocks for CalendarEvents interface (verification)
- Add CalendarEvents method to recordingAPI in auth_test.go
- Add CalendarEvents method to fakeCore in handlers_test.go

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:20:16 +04:00
kami 880715fad4 maven: long-term memory vector-store interface (task 7)
- New internal/memory/ package: Store interface, InMemoryStore (cosine sim)
- Tests: insert→search roundtrip, topK truncation, empty store, cosine edges
- Wire into voice: memStore on reactiveHandler, insert note embedding after WriteNote
- Memory Insert is best-effort, log-and-continue on error

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:18:50 +04:00
kami e030466cac maven: weather module skeleton with open-meteo provider (task 5)
- New internal/weather/ package: Provider interface, Weather struct, StubProvider
- OpenMeteoProvider with geocoding + current weather (keyless, free API)
- Config: WeatherConfig in VoiceConfig (provider, default_location)
- Wire in voice.go as weatherProvider on reactiveHandler
- Handle weather queries in IntentQuery (before notes RAG)
- Helper: isWeatherQuery / extractWeatherLocation
- Tests: mocked HTTP round-trip for OpenMeteo, stub ErrNotConfigured, config tests
- No real network calls in any test

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:15:54 +04:00
kami 428af3f3c6 maven: general-knowledge phraser routing (task 4)
- Stub.PhraseQuery: empty notes → "не знаю." (was: "no notes")
- LLMPhraser.PhraseQuery: empty notes → general knowledge prompt to LLM
- Voice handler: on notes RAG failure, try phraser before giving up
- New router.KnowledgePrompt() pure function with test

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:11:54 +04:00
kami cf066bde97 maven: calendar event querying (task 3)
Store: CalendarEvents(ctx, from, to) — filters caldav facts by key date prefix.
IPC: full wiring through interface, server dispatch, and client.
Router: ParseCalendarDate (сегодня/завтра), CalendarEventFormatter (RU reply).
Voice: calendar detection before notes RAG in IntentQuery handler.
Tests: store integration test, date parser tests, formatter tests.

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:09:47 +04:00
kami b778f0bbec maven: embedder config validation + docs (task 1)
- Tests: all-three-set → ok; one-missing → error; nil → ok
- Log message on HashEmbedder fallback in voice.go
- Document floor mode in START.md

Co-Authored-By: opencode <opencode@anthropic.com>
2026-07-06 04:02:32 +04:00
kami 1eca17f37b reminders: add recurring reminder support
Add cron expression support for recurring reminders using robfig/cron/v3.

Changes:
- Migration #2: ALTER TABLE reminders ADD COLUMN cron TEXT + next_fire_ts INTEGER
- Reminder struct: add Cron and NextFireTs fields
- scanReminder helper extracts full row including nullable cron
- CreateReminder: accept optional cron param, store next_fire_ts = fire_ts
- DueReminders: query on next_fire_ts instead of fire_ts
- RescheduleReminder: new method — parse cron, compute next fire, update
  next_fire_ts or mark fired if no more valid times
- Dispatcher: call RescheduleReminder for cron reminders, MarkReminder for
  one-shots (preserving existing behavior for ID=0 digest skip)
- ReminderCompleter interface: add RescheduleReminder method
- storeAPI adapter: forward RescheduleReminder
- All callers updated: CreateReminder signature includes cron param
- Tests: TestRecurringReminder (store), TestDispatchRecurringReminderReschedules
- Existing tests updated for new signature
2026-07-05 11:46:12 +04:00
kami 6b80fd0c0f tools: add scope column for capability model
Add a 'scope' TEXT column (default 'homelab') to the tools table so tools
can be namespaced by scope (e.g. "homelab:restart", "datacenter:reboot").
Backward-compat: bare name defaults to "homelab" scope.

Changes:
- Migration #1: ALTER TABLE tools ADD COLUMN scope
- store.Tool: add Scope field, update all SQL and scanTool()
- ipc.Tool DTO and request types: add Scope field
- CoreAPI interface: pass scope in ProposeTool/EnableTool
- storeAPI adapters: forward scope
- cmd/mavend/voice: pass scope (empty → homelab)
- cmd/mavweb/tools: show scope column in UI tables, hidden fields
- All tests updated for scope field
- Migration test made dynamic (startVer = len(migrations))
2026-07-05 11:40:15 +04:00
kami 6239eca243 items 5-7: passkey step-up, tools enable/disable, note RAG — end to end
Completes the three in-flight open items and fixes the away-fallthrough bug.

Item 7 — passkey step-up (WebAuthn):
- internal/webauthn: ES256/P-256 register + assert with real ecdsa signature
  verification, minimal CBOR/COSE decode, PasskeySession (L2→L3 on assert,
  decays after TTL). Drop the RS256 offer we can't verify (register-ok/
  assert-fail trap). Verify rpIdHash + UP/UV flags in FinishAssertion — UV is
  the step-up gesture. Round-trip test with negative cases (tampered sig,
  missing UV, wrong origin).
- cmd/mavweb: /auth/passkey enroll+assert page (the only surface that can do
  a WebAuthn gesture) + the four begin/finish endpoints. Without this the
  daemon's PasskeySession swap leaves /tools enable permanently blocked.
- daemon wires PasskeySession as the auth Session + srv.StepUp; policy gates
  MethodAssertStepUp at AuthRead.

Item 5 — tools page: DisableTool through store/ipc/client/wire; /tools grows a
disable action and a link to the passkey page. Lifecycle test.

Item 6 — note RAG: PhraseQuery on the phraser (LLM-composed answer over top-k
notes, raw-notes fallback); IntentQuery routes through it. Stub returns a
deterministic summary.

Item 2 — away-fallthrough: on ErrVoiceNoSession the dispatcher now reroutes
through the AWAY table (sev3→ntfy, sev4→telegram-repeat-til-ack, sev≤2→drop)
instead of silently dropping / mis-routing to the present-list remainder.
Covers DispatchNudge + DispatchReminder. 4 tests.

Also: re-add ProposeTool to CoreAPI (dropped in a comment rewrite), fix
missing imports + a duplicate block left mid-edit, drop dead AssertStepUpFunc,
gitignore /mavcaldav.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:41:13 +04:00
kami b77f209686 voice: pre-route quiet-hours toggle, whisper ctx cancellation, stale reply fix
- resolveQuietToggle runs in HandlePushToTalk before the router so
  'тихий режим' works regardless of classifier confidence.
- whisper_full() runs in a goroutine with ctx.Done() select so the
  handler returns promptly on timeout/shutdown.
- StubReplier.IntentQuery no longer claims query is unimplemented.
2026-07-03 10:56:44 +02:00
kami e00cb07658 fix zombie leak, add quiet-hours toggle, improve query reply, configurable router threshold, JS dashboard 2026-07-03 00:42:35 +02:00