- webauthn.go: keyIPC interface for StoreEncryptionKey/Unlock, wired
through PasskeyHandle. RegisterFinish calls StoreEncryptionKey with
the credential's public key after successful enrollment. AssertFinish
calls Unlock with the stored public key after assertion (alongside
existing AssertStepUp call).
- server.go: fix data race on s.api by switching from bare CoreAPI field
to atomic.Value. SetAPI uses Store(), dispatch uses Load(). No more
race-flagged tests.
- make test green (303+, -race)
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
Task 7 inserted note embeddings into the memory Store but nothing read them
back, and facts weren't indexed at all. Complete the read side:
- Facts are now embedded and inserted into memStore on capture (best-effort,
never fails the fact write) — the notes table can't answer fact questions
("когда я пил воду?"), so memStore is their only recall path.
- Insert meta now carries text/ts/type so a Search hit is self-describing.
- IntentQuery consults memStore.Search after notes-RAG misses and before the
general-knowledge phraser fallback (bestRecall, unit-tested). Strictly
additive: it only runs once the notes path has already given up, so it can't
regress existing recall. Note hits here overlap notes-RAG by design; the
payoff is fact recall and a real read seam for a future persistent backend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
All three are imported by non-test code (router/onnxembedder.go,
mavweb/main.go, store/reminders.go) but were labeled // indirect. `go mod
tidy` can't run in this repo (it walks the vendored deps/go toolchain tree
and errors), so correct the labels by hand.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
RevertFact voids the latest fact for a key — a store mutation — but
/api/revert had no step-up gate, while POST /tools required L3. Close the
inconsistency: thread the same *webauthn.PasskeySession into handleRevert
and reject with 403 when a configured session isn't asserted. nil session
(WebAuthn unconfigured) keeps prior behavior — transport-level auth only.
Tests: un-asserted session → 403 and RevertFact not called; asserted → 200.
The RevertFact mock now records its key so the gate assertion is meaningful.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 4 created and tested router.KnowledgePrompt() but the live path in
LLMPhraser.PhraseQuery used a separate hardcoded copy of the same RU
anti-hallucination prompt, leaving KnowledgePrompt() as dead code and two
strings that could drift. Point the phraser at the tested helper so there
is a single source of truth.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Task-7 verification commit (d52f60c) added the CalendarEvents mock
method but left fakeCore's struct block misaligned, so `gofmt -l` still
flagged this file despite the "all gates green" claim. Realign it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
- 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>
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>
- Rewrote ui.css with design tokens, card/btn/badge/dot components
- All pages wrapped in <main class=page> with max-width container
- Replaced inline <style> blocks with ui.css classes
- passkey page now uses shared nav.site template
- PWA voice page unified under shared nav.site (no more separate tab nav)
- Inline lang toggle moved from nav to voice page body
One theme (the PWA's dark palette) for dash/history/trace/notifications/
tools/passkey via static/ui.css; shared nav template with active-page
highlight; tables wrapped in .scroll so they pan on phones; PWA nav no
longer clips the RU/EN toggle; dash 'updated' timestamp fixed (selector
matched nothing). AGENTS.md documents the local preview/screenshot recipe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASstMtsZWLSRcD1Tq8T68Q
- digest queue no longer dropped on failed dispatch (retry next tick)
- collapsed reminders marked fired/rescheduled only after digest delivers
- /tools step-up gate skipped when WebAuthn is not configured (was 403 forever)
- passkey credential store rolls back memory on persist failure
- auth_test fake updated for TickTrace (branch build break)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASstMtsZWLSRcD1Tq8T68Q
RecentNudges IPC method, store adapter, dispatch, and client proxy.
Web UI at /notifications showing recent nudge history with color-coded
outcomes, nav links from /dash and /history.
New template showing the most recent tick's rule evaluation results
with color-coded table, expandable gate detail panels, nav links
to dash and history. Uses the TickTrace IPC method from #15.
ExplainTick and ExplainGate produce a full TickTrace for every tick,
recording per-rule: predicate result, gate result, first gate blocker,
gate detail (snooze/cooldown/presence etc.), and win/loss info.
IPC layer: new MethodTickTrace, DTOs (TickTrace, RuleTrace, GateDetail),
dispatch, client proxy, and CoreAPI interface method.
Daemon: tickLoop caches the latest trace; daemonAPI wraps storeAPI
with a TickTrace override that returns the cached trace.
Tests: 15 new tests for ExplainGate (all blockers, bypass conditions,
ordering) and ExplainTick (nothing fires, one fires, tiebreak,
winner/lost_to recording, gate-blocked recording).
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 local PasskeySession that handleTools checks before processing
any POST action (enable/disable). If the session hasn't been asserted
within the 5-minute TTL, return 403 Forbidden.
Changes:
- webauthn/session.go: add IsStepUp() convenience method (nil-safe)
- webauthn.go: PasskeyHandle holds a *PasskeySession; AssertFinish
calls session.Assert() after IPC step-up
- main.go: create stepUpSession, pass to handleTools and
newPasskeyHandle; handleTools returns 403 if !session.IsStepUp()
- handlers_test.go: update TestEnableTool_NoInProcessAuthGate to
expect 403; add TestEnableTool_WithAuthGate_RequiresStepUp for
the happy path with asserted session; update all 10 call sites
Split cheatsheet into separate RU/EN <dd> elements with CSS class
toggle. Toggle buttons in nav bar (RU/EN). Language also controlled
by ?lang=ru|en query parameter (default: ru).
The rest of the PWA stays English; voice responses remain Russian.
Create a minimal SVG icon (512x512, dark rounded square with blue 'M')
and update manifest.json to reference it with purpose 'any'.
This fixes the blank tile on mobile add-to-home-screen and PWA install
prompts.
Simple shell script for backing up, restoring, and verifying the
encrypted SQLite database (AES-256-GCM with MVNC1\0 magic header).
Commands:
backup — cp + magic-verify to MAVEN_BACKUP_DIR
restore — cp back with confirmation prompt
verify — check magic header via od (no xxd/jq dependency)
list — show all backups with size and validity
Portable: uses only POSIX sh, od, grep, stat. Config path resolved
from mavend.json via grep or env var overrides.
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
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))
Covers:
- StubHandler.Synthesize: PCM16kMono format, 6400-byte output,
different text → different waveform, empty text → audio
- resample22050To16000: empty input, approximate output length
- defaultSocket: XDG_RUNTIME_DIR resolution
- Integration: full synthesize round-trip via Unix socket
recordingAPI did not implement the new CoreAPI.RevertFact method
added in a02e10f, causing a build failure in auth tests.
Also adds coverage.out to .gitignore.
Tests cover all Send() code paths:
- Nil TTS synthesizer returns tts-not-wired error
- Nil sessions registry returns sessions-not-wired error
- No live session returns delivery.ErrVoiceNoSession (dispatcher reroutes)
- With session: pushes audio_nudge frame with correct kind, rule_name,
text, PCM16kMono format, non-empty audio bytes
- Empty Body falls back to Summary text
- TTS synthesize error propagates with 'synthesize:' prefix
- Invalid audio format rejects with 'refuse to ship' error
Uses net.Pipe() for real voice.Sessions integration, tts.Stub for
deterministic synthesis, and fake synthesizers for error paths.
- New store.VoidLatestFact() method finds latest non-voided fact for
a key and writes a void-marker row pointing at it (transactional).
- New IPC method MethodRevertFact with CoreAPI.RevertFact interface,
storeAPI adapter, server dispatch, and client proxy.
- New HTTP endpoint POST /api/revert?key=<key> in mavweb.
- History page adds a 'revert' button per non-voided fact row with
JS confirmation and optimistic UI (marks row voided on success).
- All existing store, IPC, and mavweb tests pass.
When the daemon starts after being offline, multiple due reminders
would fire simultaneously as separate notifications. Now a single
digest reminder is dispatched instead, summarizing all pending items.
- New collapseReminders() helper in gather.go: if 2+ reminders are
due, marks originals as fired and returns one synthetic reminder
(ID=0) with a combined JSON payload.
- Dispatcher skips MarkReminder for ID=0 (synthetic digest).
- All loop and delivery tests pass.
- New /history route displays recent facts in a dedicated page with
voided-row styling (line-through + opacity + badge).
- Navigation link added to /dash page header.
- Handler calls core.RecentFacts(ctx, 200) and renders historyTmpl.
- New credentialStore type in credentials.go loads/saves
map[id]localCred to a JSON file. Thread-safe with sync.RWMutex,
writes to disk on every mutation.
- PasskeyHandle replaces sync.RWMutex+map with *credentialStore.
Inline save/lookip/update closures delegate to store methods.
- newPasskeyHandle now takes a storePath parameter and returns an
error; callers updated.
- New -passkey-file flag (default ./passkeys.json) configures the
credential store path in main.go.
- Tests use os.CreateTemp in t.TempDir() so each test gets an
isolated, auto-cleaned store file.
- test target now runs with -race and emits coverage.out for coverage
reporting.
- New build-caldav target builds cmd/mavcaldav without CGO (pure Go).
- build-caldav added to PHONY and build dependency chain.
- mavcaldav added to clean target.
The cold-start crash-loop wasn't mavweb-specific — mavpoll and mavcaldav also
ipc.Dial + exit on failure, so they crash-looped until core booted too. Moved
the retry into ipc.DialWait (capped backoff, bounded) and switched mavweb,
mavpoll, mavcaldav to it. mavweb's local dialCoreWithRetry is gone.
Test: server appears after DialWait starts → it waits and connects.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>