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
A table per intent (reminder needs a time, fact needs a key, act needs a fn)
plus one fixed Russian question per slot. Templates, not model output: a 0.8B
would wander and a question that rewords itself is harder to answer. Note,
query, chat and system get no question — for those a clarify decision keeps
the canned reply rather than inventing a question for noise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
The review's verification plan needs an instrument to settle whether the
classifier or the LLM router handles RU queries better, but that comparison
only means something if the daemon's own safety invariants are pinned
independently first.
These scenarios are deliberately narrow. They consume already-normalized
router decisions and assert what the post-router daemon owns: that a decision
requiring confirmation cannot execute before it is confirmed, that an
unresolved entity is never guessed at, and that named capabilities stay
unexecuted. Model routing quality is a separate question, evaluated against a
held-out contract fixture — mixing the two would produce a suite that fails for
two unrelated reasons.
The fixture is versioned (schema_version) so scenarios can be added without
rewriting the loader.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
Backlog item #3 (20-07-2026-BACKLOG.md). A morning routine is a checklist for
a daily window: several items, each evidenced by a fact key, completed in any
order, checked once near the end of the window. Modelling it as four
independent reminder timers would stack into exactly the kind of noise Maven is
supposed not to produce, so the engine nags at most once per day per routine
and only for what is actually still missing.
internal/morning follows the established pure-engine pattern (loop, routine,
pattern): no store, no clock of its own. Evaluate answers "what's still
missing" at any point; Due decides whether to nag. The impurity — reading
facts under the store lock, holding the last-nudge map across ticks — stays in
the tick driver, which calls Due each tick exactly as it does for loop.Rule
and routine.Routine.
Completion evidence is a fact key's latest non-voided value timestamped inside
today's window, so manual ("выпил воды", voice-tapped) and inferred (another
daemon writing the same key) are indistinguishable and both count. Weekdays
scopes which days a routine applies to, so weekday/weekend variants are two
routine rows rather than a special case in the engine.
Exposed read-only: a MorningStatus RPC over ipc, and a /morning page in mavweb
built on the same server-rendered shape as /trace — no live-update loop, since
checklist state moves on the scale of minutes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
The most load-bearing decision in the project was stated four incompatible
ways: the docs said Qwen3-1.7B, deploy/mavend.json said Qwen3.5-2B, the repo's
models/llm/ held an LFM2.5-1.2B gguf, and five code comments still said LFM.
Answering "which model is deployed" meant re-deriving it from scratch every
time.
Two facts the review missed, found while resolving it:
- /mnt/hdd1/llms is bind-mounted over /opt/maven/models/llm, which shadows the
repo's models/llm/. The LFM2.5 gguf sitting there was never loaded by
anything, so it was not evidence of the deployed model at all.
- That library holds Qwen3.5-0.8B, -2B and -4B, and no Qwen3-1.7B. The config
pointed at a file that does exist; the docs' Qwen3-1.7B was the stale claim,
the reverse of the assumed direction. Qwen3-1.7B is the CPT target, and that
training is still in flight (Vikunja #122), so no such gguf exists yet.
phraser.model_path moves to Qwen3.5-0.8B (Q4_K_M) — the smallest checkpoint on
disk, chosen for latency, and relevant to whether the LLM router is affordable
on this box. Docs and comments now say the same thing in one voice: 0.8B
resident now, CPT'd Qwen3-1.7B as the target, and the bind-mount shadowing
written down so the next reader does not mistake models/llm/ for ground truth.
Comments name the model, never a filename, so a swap stays a one-line config
change.
n_gpu_layers: 99 is correct and stays — compose passes /dev/dri and the render
gid for Vulkan offload to the Vega iGPU. CLAUDE.md's "CPU-only" was the stale
half of that contradiction and is corrected.
phraser.go also dropped a wrong "sub-1b, prompted not trained" size claim: the
target is trained end-to-end (RU CPT + joint persona/router SFT).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
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
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
Adds fakeecosystem_test.go: a shared fakeServer wrapping httptest.Server
with request capture, a runtime-toggleable fault (SetFault) that makes a
running fake Nexus/Praxis/Hexis fail closed like a real outage without
tearing the server down, protocol fixtures for each service's documented
response shapes, and a settable fakeClock for time-dependent assertions.
Uses it in ecosystem_harness_test.go to cover a gap the existing ad-hoc
per-test httptest servers didn't reach — handlePraxisAct had zero test
coverage — plus a fault-then-recovery test showing the same fake flapping
mid-session, the shape the earlier fail-closed fixes (#272/#273) need
regression coverage against.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
Complements Praxis's new entity_id filter on /tools/attention: lets
callers that already hold a resolved Nexus entity_id (e.g. after
resolveEntityReference) ask what needs attention for that entity
directly, instead of filtering the unscoped list client-side.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
mavend's hand-rolled Nexus and Praxis HTTP clients sent bare requests
with no version or correlation headers, unlike the Hexis client.
Added a shared context-based correlation ID mechanism and version
headers (X-Nexus-Version, X-Praxis-Version) across all Nexus/Praxis
call sites, and threaded the correlation ID already generated in
executeCapability through to the Nexus/Praxis calls in the same
request chain. Synced vendor/ copy of hexis/pkg/client after its
WithCorrelationID addition.
Part of Vikunja #273.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
Companion to the dispatcher-side outbox change: adds the
delivery_attempts table migration, Store.BeginDeliveryAttempt/
CompleteDeliveryAttempt/ReconcileStaleDeliveryAttempts, and wires
ReconcileStaleDeliveryAttempts + Config.Outbox into mavend startup
before the tick loop resumes.
Vikunja #270.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
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
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.
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>
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>
- Add reactive_notes_test.go: tests for context-aware reactive nudge
generation using LLM phraser with dialogue history.
- Add docs/plans/2026-07-10-router-lfm-foundation.md: architecture research
on replacing classifier cascade with LFM-based router.
- 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.
Store layer: ListReminders returns the n most recent reminders
(newest first). IPC: new MethodListReminders wired through server,
client, and lockedAPI. Web: /reminders page with table of created
time, fire time, status badge, and payload text; empty state with
prompt to ask maven for a reminder. Sidebar entry under Automation.
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.
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.
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)
- 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).
- 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
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>
- 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>
- 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
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 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))
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>
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>
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.
- 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.