1191 Commits

Author SHA1 Message Date
kami a2835bbdf6 Cover every cell of the delivery routing table
Table-driven tests for all four severity bands crossed with present and away,
both as the pure table and end to end through the dispatcher. Three tests are
written to DESIGN.md and skipped because the code does not keep the claim: the
care-away drop is recorded nowhere, and the minimal body is enforced per-sink
rather than by the dispatcher. Also gofmt'd dispatcher_test.go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:24:43 +04:00
kami c262c1e4c0 Merge the routine accept path and store gaps 2026-07-31 02:24:07 +04:00
kami a33ad82178 Let the /routines page accept a proposal, gated at step-up (#46)
Accepting a routine gives the trigger loop a new standing reason to speak to
the human, so it is the same authority tier as enabling a tool and shares the
stepUpOK gate; dismiss only ever makes maven quieter, so it is ungated.
Look at handleRoutines and acceptRoutine in cmd/mavweb/main.go: accept creates
the recurring reminder, then links it via the new ipc AcceptProposedRoutine.
The page now says what maven noticed in her own words (pattern.PhraseRoutine).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:23:28 +04:00
kami af35ec3629 Work out which slot is missing and phrase one short question
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
2026-07-31 02:22:18 +04:00
kami 9145b83100 Add the clarify data layer: a parked question with one missing slot
The router can already say "I am not sure" (Decision.Clarify) but the daemon
had nowhere to keep the request while it asked. PendingQuestion holds the
original slots, ClarifyStore parks one per dialogue id with a 90s TTL, and
Answer fills only the slots that were missing so an answer can never rewrite
what she already understood. Logic that uses this comes next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:20:28 +04:00
kami 43470abc57 Add a held-out note-recall harness (fixture + scorer)
Measures whether Maven can find the right note again from a paraphrased
question. Review internal/memory/recalleval/recalleval.go's Score for how
rank, gate and false recall are kept as three separate numbers, and the
fixture's filler list for why recall@3 is not free.
Fixture JSON is generated data and does not count toward the diff limit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:18:58 +04:00
kami 94eb92fb15 Label LLM eval runs with the model llama-server has loaded
The bake-off in #278/#250 needs two models' scores side by side, and the
report names only carried the config, so the rows were indistinguishable.
ModelID reads /v1/models instead of taking a string that goes stale.
New target: make eval-models MAVEN_LLM_URL=...

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:17:20 +04:00
kami 707c3e5040 Ignore deps and models as symlinks, not just directories
.gitignore had deps/ and /models/llm/ with trailing slashes. A trailing slash
only matches a real directory, so a *symlink* with the same name is not ignored
and git add -A commits it as a symlink blob.

That bites anyone working in a git worktree, where deps/ and models/ do not
exist and have to be linked in from the main checkout. It already happened once
tonight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:17:15 +04:00
kami ace9fbbc06 Merge the llm_router flag and the kill-maven fix 2026-07-31 02:17:00 +04:00
kami 3884db33e9 Give proposed routines a status filter and pin down the dedup rule (#46)
Look at internal/store/proposed_routines.go: status flips in place with an
`AND status = 'proposed'` guard, not append-only like facts/voids_id — a
proposal is a question with one answer, same shape as tools.status. The
UNIQUE(action, object) key is what stops a dismissed routine coming back.
New tests cover re-propose-after-dismiss and listing by status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:16:39 +04:00
kami dfb8d26b62 Fix kill-maven.sh so it actually kills llama-server
The MODEL default was LFM2, but the deploy runs Qwen3.5-0.8B, so the
pkill pattern matched nothing and the server survived every kill.
Now matches any llama-server serving a .gguf, so changing the model in
deploy/mavend.json cannot break the script again. MODEL still narrows it.

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 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 6d9aa83b6e Merge the proactive rule and gate tests 2026-07-31 02:15:15 +04:00
kami f75072175d Merge the pending-question data layer 2026-07-31 02:14:33 +04:00
kami 39d83a33e8 Add the pending-question data layer for slot clarification
PendingQuestion plus ClarifyStore: same shape, locking and expiry as SessionStore. Answer fills only the missing slots and never overwrites a filled one. No wiring yet — TODOs mark the daemon hooks.
Reviewer: MaxAttempts is 1 on purpose (Maven asks once, she is not a nag).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:13:48 +04:00
kami 9d8fcf42f3 Test the universal restraint gate, including two gaps
Pins the conservative side of Gate(): quiet hours, away, calendar-busy,
cooldown and snooze, plus one nudge per tick at max severity. Reviewers: the
two skipped tests at the bottom are real gaps, not flakes. Reminders ignore
snooze (loop.go:120) and the Gatherer never fills SnoozeUntil (gather.go:153),
so snooze does nothing at runtime. No behaviour was changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:13:46 +04:00
kami 925ce223a0 Add a Value slot to dialogue.Slots
router.Slots already carries the fact payload; the dialogue copy did not, so a clarifying answer had nowhere to put it. InheritSlots carries it like Key.
Reviewer: check the new inherit block does not overwrite a filled value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:13:40 +04:00
kami 9e2af3286b Unit-test all five proactive rule predicates
Table-driven tests for water, meal, break, service_down and netdata_critical,
straight against the predicate with a fake State. Reviewers: the no-data rows
(every rule must stay quiet when its key is missing) and the ops forgery rows,
where a fact with the right value but the wrong source must be refused.
No rule fired on missing data, so no fix was needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:12:25 +04:00
kami d30618ecb7 Stop the router repetition loop
Route now sets RepeatPenalty on the request, and the grammar's string rule is
capped at 120 characters. Two of 76 fixture cases looped one sentence inside
the text field until MaxTokens, which cut the JSON in half.
Reviewers: the new constant and the grammar string rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:11:38 +04:00
kami 17b47ce206 Route questions to query, not fact
The router prompt tested "reports current state -> fact" before "wants
information -> query", so a question naming a fact key was written as a fact.
Query now comes first, plus an explicit question test.
Reviewers: the prompt block in llmrouter.go, and the note about the
training-side copy of the prompt that needs the same edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
2026-07-31 02:10:37 +04:00
kami abe9b28719 Write up the routing evaluation findings and next steps
Settles the measurement half of Vikunja #319: the resident 0.8B routes
better than the deployed classifier (50.0% vs 36.8% intent-only) at ~27x
the latency, and neither path can refuse an ambiguous utterance.

Records the four-configuration comparison, the per-case evidence behind
each finding (query->fact x15 traced to routeSystem's rule order, the
five missed-clarify cosines, the two text-field repetition truncations),
the two hypotheses that were tested and closed (thinking mode, grammar
array runaway), and a next-steps list mapped to #319/#320/#359.

#320 should not flip as-is: it would remove the refusal lane rather than
improve it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-31 00:55:32 +04:00
kami 46259b4571 Score the LLM router on Qwen3.5-0.8B against the routing fixture (#319)
Completes #319's comparison. Three configurations, because "the LLM router"
was ambiguous: the model alone, the cascade #320 would actually ship (stage-0
grammar → model → classifier floor), and a thinking-off diagnostic.

                      intent-only  full    RU     missed-clarify  p50
  classifier+onnx     36.8%        36.8%   25/61  5/6             31ms
  llm-only (0.8B)     48.7%        23.7%   13/61  6/6             850ms
  cascade+llm (0.8B)  50.0%        32.9%   18/61  6/6             825ms

On the question asked — does the resident model route better? — yes, 50.0%
vs 36.8% intent accuracy. REARCH.md's premise holds. It costs 27x the
latency (p50 825ms vs 31ms, max 3.1s), on the same llama-server the phraser
needs, so it is a trade rather than a free win.

Three things the numbers surface that the headline hides:

query→fact x15 is the dominant failure, four times the classifier's x4 on
the same axis. routeSystem's decision order puts "сообщает или обновляет
состояние" (rule 3) above "хочет получить информацию" (rule 4), so any
utterance naming a fact key matches the earlier rule and a question about
past state reads as an assertion of it. A prompt fix, not a model limit.

The LLM router cannot clarify: llmrouter.go hardcodes Confidence 1.0, so
stage 3's gate can never fire on its decisions — 6/6 missed. With #359's
finding that the classifier's gate is miscalibrated under ONNX, neither path
currently refuses. Flipping #320 as-is removes the refusal lane.

The gap between 50.0% intent and 32.9% full accuracy is entirely slots: the
LLM path fills neither Fn nor Time (it returns Slots.Text for acts, and
Extract never runs on an LLM decision).

Also settles a hypothesis rather than leaving it in the air: thinking mode is
a non-issue under a grammar (identical score), and the grammar's unbounded
("," ws action)* repetition that ran away in an isolated smoke test does not
reproduce under the real prompt — 2 errors in 76, not 76. internal/llm
deliberately does not grow a chat_template_kwargs field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-31 00:50:20 +04:00
kami d34fdf40aa Score the routing fixture with the ONNX embedder (Vikunja #319)
The onnxruntime .so was already vendored at deps/onnxruntime-linux-x64-1.26.0
— nothing to download. make eval-router now defaults MAVEN_ONNX_LIB there, so
both baselines run by default and only a fresh clone without deps/ falls back
to the hash ratchet alone.

Prod-representative result, deployed 0.55 gate: 28/76 (36.8%), RU 25/61,
EN 3/15, hard 0/11 → 4/11, p50 31ms / p95 71ms. Versus the hash floor's
13/76 at p50 9µs.

The finding is not the accuracy, it's the refusal lane: missed clarifies went
0 → 5 of 6. Better embeddings raise cosine everywhere, so the 0.55 threshold
that used to hold ambiguous utterances back stops holding — "сделай это"
routes to act at 0.847, "бэкап" to chat at 0.755. The gate was implicitly
tuned to the hash floor's low similarities. That is an argument about the
threshold, not about the embedder, and it lands before #320 rather than after.

Also fixes a fixture-model mismatch: ReminderGrammar deliberately skips the
extractor at stage 0 and the daemon's applyAction parses the time downstream
(stage0.go says so). Charging the router for that slot made 4 exact-match wins
read as misses; they are now counted as SlotsDeferred instead. Hash baseline
moves 13/76, ratchet to 0.15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-31 00:34:29 +04:00
kami c7c44229a2 Add held-out RU routing fixture and scorer (Vikunja #319)
#319 asks for a measurement before #320 flips the route decider from the
classifier cascade to the resident model. There was nothing to measure
against: the only routing tests assert single utterances, and the
classifier's seed corpus is its own training set — scoring it there
measures memorisation of frozen centroids, which is the illusion that hid
the weak RU query handling in the first place.

internal/router/eval is a separate package so both paths can be scored
from outside router (including cmd/mavend, where the real llama-server
client lives). The fixture is embedded; the scorer takes a Router
interface, so *router.Router and a bare LLM stage both go through the same
76 cases.

The fixture is a CONTRACT, not a snapshot: cases the cascade fails today
stay in the file and fail loudly. TestFixtureIsHeldOut enforces that no
utterance appears verbatim in models/seeds/*.txt.

Baseline, hash embedder at the deployed 0.55 gate: 9/76 (11.8%), 63 false
clarifies, 0 missed clarifies, p50 9µs. Almost everything falls to the
confidence gate — the documented floor behaviour, not a new bug. The
number worth comparing is TestONNXBaseline's (skipped without
MAVEN_ONNX_LIB); the assertions here are a regression ratchet plus a tight
bound on the dangerous direction: ambiguous utterances must not start
being routed confidently.

Seeding is order-fixed on purpose — a few phrases appear under two intents
and map iteration handed them to a different centroid each run, which made
the score jitter between 9 and 10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-31 00:28:44 +04:00
kami 56c87b9e79 Record the 2026-07-20 session and the 2026-07-30 senior review
PROGRESS.md gains the ecosystem-hardening session log: entity-aware fact
resolution, the typed Praxis lifecycle client, the durable delivery outbox,
fail-closed IPC handling, correlation IDs, and the fake-ecosystem test harness.
20-07-2026-BACKLOG.md marks item 2 (morning routine engine) as core-engine
done, with the wiring and the read-only /morning page described.

REVIEW-30-07-2026.md is the review the preceding commits act on. Note two of
its findings were wrong on the details and are corrected in the commits rather
than in the document: the covdata failure was upstream Go tool-shipping
behaviour, not a truncated cache, and the resident-model contradiction ran the
opposite way — deploy/mavend.json pointed at a file that existed while the docs
carried the stale claim, because /mnt/hdd1/llms is bind-mounted over the repo's
models/llm/.

One caveat, since the review itself argues against dated markdown accumulating
at the root: this file should go the same way as the SESSION-*.md logs once its
findings are closed. Two remain open — the kuma key rotation and the LLM router
evaluation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-30 23:49:44 +04:00
kami 4595e0dffd Enable nudge digestion in the deployed config
Turns on the existing digest path on homesrv: a 30m batching window, at most 5
items per flush, and a severity ceiling of 2 so anything more urgent still
goes out immediately instead of waiting for the batch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-30 23:49:22 +04:00
kami 122d0bb55c Add daemon-side safety scenario tests driven by a JSON fixture
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
2026-07-30 23:49:22 +04:00
kami 20184874b2 Add the morning routine engine — a daily checklist, not four timers
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
2026-07-30 23:49:10 +04:00
kami 76a6a007ef Pin the resident model to Qwen3.5-0.8B and name Qwen3-1.7B as the target
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
2026-07-30 23:40:33 +04:00
kami e0d0244fa9 Fold SPEC/maven/ROADMAP into DESIGN.md and drop the stale session logs
15 root markdown files, ~4,900 lines against ~33,000 lines of Go, with at least
three pairs contradicting each other. When five documents describe the
architecture, the code becomes the only trustworthy one — which defeats the
point of having them. That drift is why the resident-model question had four
incompatible answers.

SPEC.md, maven.md and ROADMAP.md are deduped into DESIGN.md rather than
concatenated, with a "Superseded" section carrying eight retired decisions and
what replaced each: classifier-owns-the-route (the cascade is still the live
path, but as a stopgap, not a design to extend), faster-whisper/vosk/silero,
the small-model phrasing claim, sqlcipher, the Kotlin/Spring sketches,
obsidian->chroma, script deployment, and FloorEnrollment. Superseded material
is kept and marked rather than deleted, so it cannot read as current.

SESSION-05/06-07-2026.md and PLANS.md are removed outright — git history holds
them, and both were verified tracked before deletion.

Go doc comments citing the deleted files are repointed to the equivalent
DESIGN.md sections. Several asserted designs that were already retired, so the
claims are corrected and not just relinked: stt.go named faster-whisper as
production (it is whisper.cpp), tts.go named silero (it is piper), intent.go
still described the classifier as owning the route, and stale vosk/chroma
vocabulary is replaced. ECOSYSTEM-SPEC.md references are deliberately
untouched — that is a different document, and a naive grep for SPEC.md matches
it.

Root markdown drops from 4,880 to ~3,700 lines. The review's ~1,500 target is
not reachable while keeping the files it also said to keep — those alone are
2,553 lines — so trimming further needs a separate decision on
MAVEN_ECOSYSTEM_ARCHITECTURE.md and PROGRESS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-30 23:39:56 +04:00
kami 691c3660d2 Bind mavweb to loopback and make the step-up fail-open loud and overridable
The /tools enable action takes name+cmd from form fields and calls
core.EnableTool, so it defines arbitrary argv that internal/tool then executes.
Its step-up gate read `session != nil && !session.IsStepUp()`, and
stepUpSession is nil unless both -webauthn-origin and -webauthn-rpid are set —
so with neither flag the gate was skipped entirely. compose passed neither and
published 9201 on every host interface, while /ptt proxies to the voice server
unauthenticated, so a caller could enable a tool, trigger it, and answer its
own confirm turn. internal/tool's boundary reasoning ("a compromised router
can't grant itself a capability") held; the outer boundary it depends on was an
unwritten deployment assumption.

The fail-open itself stays: gating on a session that can never be asserted
would 403 permanently, and that reasoning is sound. What was missing is the
compensating control.

- compose publishes 127.0.0.1:9201 so reaching the UI requires the wg tunnel by
  construction rather than by convention. Verified no other service reaches
  mavweb by host-published port; mavpoll is host-networked but only dials
  netdata and kuma.
- stepUpOK() replaces the two inline gates in handleTools and handleRevert, so
  one decision point covers both surfaces.
- -require-stepup (default false, behaviour byte-for-byte unchanged) fails those
  actions closed when step-up cannot be asserted.
- A startup warning names both unguarded surfaces when stepUpSession is nil,
  in fail-open and fail-closed variants.

Also repoints one doc comment at DESIGN.md, since it shared a hunk with the
warning block.

The committed kuma key is deliberately left for a separate change: the old
value is in git history forever, so rotation means a genuinely new key, not a
re-commit under a variable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-30 23:39:07 +04:00
kami fb8b470f78 Make the vendored Go toolchain self-sufficient so make test exits 0
`make test` failed with `go: no such tool "covdata"` on the two packages that
have no test files (cmd/mavenclient, cmd/mavend/seedtest), even though every
package passed. A permanently-red test target trains you to ignore the exit
code, which is the signal protecting the whole suite.

The cause was not a truncated download. Go >=1.24 ships only 7 prebuilt GOROOT
tools; covdata, pprof, test2json, nm, objdump and trace ship as source and are
built on demand. `go tool covdata` has that build-on-demand fallback, but the
`-coverprofile` merge path goes through base.Tool(), which only stats
pkg/tool and exits. deps/go was go1.23.4 against a `go 1.25.5` directive, so
GOTOOLCHAIN=auto re-exec'd into a downloaded toolchain module and inherited
the gap.

deps/go now holds a checksum-verified go1.25.5 with those 8 tools built in
from its own source. Pin GOTOOLCHAIN=local so a future go.mod bump cannot
silently re-exec into a tool-poor module toolchain again, and add a deps-go
target so the install is reproducible rather than hand-placed — which was the
point of vendoring it. pprof works now too, which matters on a
latency-sensitive box.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5JApcrCRVGmqrxnhynSik
2026-07-30 23:38:38 +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 03fa52dfd4 Add entity-aware fact resolution against Nexus (Vikunja #279)
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
2026-07-20 12:00:37 +04:00
kami c932cd8677 Add reusable fake-ecosystem test harness with fault injection
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
2026-07-20 11:33:47 +04:00
kami 0579ef94f7 Add entity-scoped attention query to Praxis client
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
2026-07-20 11:22:35 +04:00
kami b743860fdd Add version/correlation headers to Nexus and Praxis ecosystem clients
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
2026-07-20 11:12:01 +04:00
kami 9ff726ee26 Wire durable delivery outbox: migration, store, and dispatcher config
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
2026-07-20 03:09:33 +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 29f23e3715 Add durable delivery outbox: Begin-before-send, Complete-after, unknown on crash
Closes the audit finding: a crash between 'sink accepted it' and 'we
recorded that' caused duplicate sends on the next tick with no trace.
BeginDeliveryAttempt now runs before Send, CompleteDeliveryAttempt
after — a stale 'pending' row found at startup reconciles to 'unknown'
(never silently resent, never silently dropped, same rule as the Hexis
execution engine's timeout handling). Wired into DispatchNudge,
DispatchReminder, and RepeatUnacked; ReconcileStaleDeliveryAttempts
runs once at mavend startup before the tick loop resumes.

9 new dispatcher tests cover begin-before-send ordering, failed-send
completion, the reminder path, and begin-failure not blocking send.

Vikunja #270.

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 838fde1fff Fail closed on ambiguous IPC mutation outcomes instead of blind retry
Vikunja #269 (P0): Client.call() retried any connection-loss uniformly,
including the case where the request frame was already sent and the reply
never arrived — the server may have already committed the write before
dying, so a blind retry could double-apply it. This violates the ecosystem
rule against retrying an unknown mutation outcome.

- Split errConnLost into errWriteLost (request never sent — always safe to
  retry) and errReadLost (request sent, reply lost — ambiguous).
- errReadLost is only auto-retried for read-only methods (replaying a read
  can't double-apply). A mutation method instead returns ErrAmbiguousOutcome
  so the caller can decide, rather than the boundary silently guessing.
- Added commit-then-disconnect regression tests: a mutation (WriteFact)
  surfaces ErrAmbiguousOutcome and does not retry; a read (Presence) retries
  transparently past the same disconnect timing.
2026-07-20 01:12:10 +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 44421d2048 Add backlog notes for post-model reliability/context/reach work
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ghELqYhZNLub2TXGMazqA
2026-07-20 00:51:11 +04:00
kami 1d0cdebd20 fix(mavweb): stable panel refresh on /ecosystem
The auto-refresh replaced cards by index while mutating the parsed
document — each replaceWith detached a card from the fetched doc,
shifting the remaining indices, so Praxis got dropped and Hexis
rendered twice after the first tick. Give the three sections stable
ids and replace by getElementById (dash.html's proven pattern).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:12:07 +04:00
kami 5fe8f228c1 feat(mavweb): /ecosystem page consuming Nexus/Praxis/Hexis + shell fixes
Add a read-only /ecosystem page that consumes the sibling services'
JSON APIs (Nexus entities, Praxis attention, Hexis capabilities),
fetched concurrently with honest per-panel error states. Siblings stay
headless — mavweb is their human surface (arch §16). Wired via mavweb
-nexus/-praxis/-hexis flags; mavweb joins the ecosystem compose network.

Fix mobile horizontal overflow across all pages: .content is a flex
child with default min-width:auto, so it refused to shrink below the
tables' intrinsic width. min-width:0 lets wide tables pan inside .scroll
instead of dragging the page sideways. Verified via CDP geometry check
(scrollWidth === clientWidth at 430px).

Also includes in-progress Ethos UI redesign, ecosystem deploy compose,
and planning docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 22:04:23 +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 0c65387a5f feat(router): array route contract + shorter RU router prompt
Route contract is now a JSON array of action objects (one per ask) so
compound utterances route all their intents, not just the first. Grammar
root emits `[{intent...},...]`; parseActions tolerates a bare object.
Cascade still returns one Decision — full N-action dispatch lands with the
engine turn-on (marked in-code).

Router prompt rewritten shorter + decision-ordered (prompt-guy feedback),
fact redefined as "implicit update" not "trackable state", kept in Russian
to match the CPT base + phraser. "интент" → "намерение".

CLAUDE.md: routing-architecture section + refreshed open items.
docs/plans: route-data generation plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GMrVfuYN3nE4L1vEiFYC9
2026-07-11 23:27:44 +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 22b43c07a9 feat: reactive notes test and router LFM foundation plan
- 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.
2026-07-10 15:50:01 +04:00
kami 62b9098c87 chore: gitignore /models/llm/ (large binary models) 2026-07-10 15:49:58 +04:00