facts gain a Subject/EntityID/ResolutionState triple and an async
enrichment worker that resolves free-text subjects to canonical Nexus
entity_ids, mirroring Praxis's enrichment-worker pattern. Ambiguous or
unreachable Nexus never guesses an entity_id — the fact stays pending
or terminal-ambiguous instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
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>
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)
- 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.
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
- 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>
- 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>
When enabled, eligible nudges (sev ≤ ceiling) are queued in memory
instead of sent immediately. Every 'window' duration (or when
'max_items' reached), the queue is flushed as a single digest
notification with concatenated bodies.
Config:
digest:
enabled: true # default false
window: 30m # flush window (default 30m)
max_items: 5 # flush at this count (default 5)
severity_ceiling: 2 # max sev batched (default 2; sev3+ bypass)
Changes:
- config.go: add DigestConfig struct with defaults
- tick.go: QueuedNudge type, digest queue/flush in TickLoop,
shouldQueue/maybeFlush/flushDigest helpers
- main.go: pass cfg.Digest to newTickLoop
- tick_test.go: 6 new tests covering queue, flush, bypass, dedup
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))
Two spine infra items (feature-ranking #1, part of the migration prereq):
- migrations.go: PRAGMA user_version runner, empty (no-op) migration slice,
one tx per step, fail-closed. Mechanism in place before any real schema
change needs it.
- crypt.go: file-level at-rest encryption. On-disk file is always AES-256-GCM
ciphertext; decrypted to a tmpfs working copy modernc sqlite operates on;
re-encrypted atomically on Close, plaintext wiped, key zeroed. Pure stdlib,
CGO stays off. Fails closed on wrong key/tamper, never falls back to
plaintext. Key is a 32-byte seam (config db_key_b64/db_key_env today; the
passkey-derived L3 cold-start key plugs into the same seam later).
Chosen over cgo SQLCipher (would force libsqlcipher + CGO across the project)
and over the ncruces page-level VFS (swaps the driver project-wide); noted as
the upgrade path in a ponytail: comment. Threat model is disk-at-rest only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds QuietHours config section (start/end as HH:MM local time). The loop's
gatherer checks the window each tick: if now falls within [start, end), the
State.QuietHours flag is set to true regardless of the config fact (which
the voice toggle writes independently). Both sources activate quiet —
schedule AND toggle.
Handles midnight-crossing windows (23:00-08:00). The gate already reads
State.QuietHours for care nudge suppression — no gate change needed.
piper_handler: close stdin/stdout pipes on Start() failure and on
WriteString error instead of leaking fds. Propagate WriteString error.
worker/client: log SetDeadline errors instead of discarding them.
voice/session: pushAudio marshals params inline and returns the marshal
error instead of swallowing it via mustParams (removed).
tool/matcher: log ListTools errors instead of silently returning an
empty allowlist that refuses every act.
config: applyDefaults now sets RouterThreshold and ToolTimeout defaults
so consumers self-contained defaults are belt-and-suspenders.
worker/server.go: dispatch now receives a per-connection context instead of
context.Background(), so handler cancellation propagates on conn close.
voice/server.go: same — per-conn context fed through safeDispatch into
HandlePushToTalk instead of context.Background().
store/reminders.go: propagate LastInsertId error.
store/nudges.go: propagate LastInsertId and RowsAffected errors.
store/tools.go: propagate RowsAffected error.
config/config.go: applyDefaults now respects StateDir when set, using it as
the base for empty DBPath/SocketPath instead of silently ignoring it.
phraser/llmphraser.go: close stderr pipe fd when cmd.Start() fails.