Files
Maven/docs/audits/current-state-audit.md
T

37 KiB
Raw Blame History

Maven Current-State Audit

Date: 2026-09-05 Scope: Read-only code + tests investigation. No inference from names or docs. Source of truth: code and tests only.


A. Current End-to-End Flow Diagram

                         Telegram Bot API (long-poll)
                                  |
                                  | api.Chat(ctx, "telegram:<id>", text)
                                  v
mavweb POST /api/chat --> ipc.Client (unix socket) --> daemonAPI.Chat()
                                                             |
                                                             | chatFn = handler.handleText
                                                             v
Voice TCP :9100 --> HandlePushToTalk --> STT -->  +------------------+
                                                  |   runTurn()     |
                                                  | voice.go:270    |
                                                  +------------------+
                                                  | 0. decision record (ctx)
                                                  | 0b. turnRoute (computed once)
                                                  | 1.  expired clarify notice
                                                  | 2.  resolveConfirm (y/n)
                                                  | 3.  resolveRepair (correction)
                                                  | 3b. resolveUntargetedRepair
                                                  | 3c. resolveCommandProhibition
                                                  | 4.  resolveClarifyAnswer
                                                  | 5.  resolveQuietToggle
                                                  | 5b. resolveSnooze
                                                  | 5c. resolveAck
                                                  | 5d. resolveReminderCancellation
                                                  | 5e. resolveCandidate (ordinal)
                                                  | 6.  ROUTE (cascade)
                                                  | 7.  dialogue merge (followUpMerge)
                                                  | 8.  clarify (missing slots)
                                                  | 9.  applyAction (per-intent dispatch)
                                                  | 10. replier (LLM or stub)
                                                  +------------------+
                                                             |
                                                             v
                                                       reply string

The routing cascade (step 6) in detail:

utterance
  |
  v
Stage 0: Grammars (stagezero.go:24-91)
  | 22 ordered regex/structural grammars. First match wins.
  | Confidence = 1.0, Stage = 0. fillMatchedSlots runs.
  | NO MATCH -> fall through
  v
Stage 0b: Routing Heads (heads.go, ONNX)
  | 4 linear heads over mean-pooled e5-small: intent, destination, slot BIO, clarify
  | Below headsThreshold (0.6) -> decline, cascade continues
  | Intent head softmax -> Decision{Intent, Confidence, Source, Clarify}
  | completeParsedSingleVerbFact can overrule clarify
  | NO WIRE / ERROR / DECLINE -> fall through
  v
Stage 1a: LLM Router (llmrouter.go, Qwen3-1.7B via llama-server)
  | GBNF grammar-constrained JSON output
  | gateLLMDecision: thin confidence (0.3) for incomplete slots
  | ERROR / PARSE FAIL / "unknown" -> fall through
  v
Stage 1: Nearest-Centroid Classifier (classifier.go)
  | Cosine similarity over ONNX embeddings against seed centroids
  | Best intent wins (ties broken by name)
  v
Stage 2: Slot Extraction (slots.go:55-91)
  | Dispatch on intent: DateTimeParser (reminder), ActMatcher (act), FactParser (fact)
  v
Stage 3: Confidence Gate (router.go:244-248)
  | Confidence < threshold (0.55) -> Clarify = true, Stage = 3

B. Package/File Ownership Map

Command packages (cmd/)

Package Binary Role
cmd/mavend mavend Core daemon: DB, IPC, routing, actions, voice server, tick loop
cmd/mavweb mavweb Web UI + HTTP server (serves /, /dash, /history, /trace, /notifications, /tools)
cmd/mavwaked mavwaked Wake-word + voice activity detection (silero VAD, energy threshold)
cmd/mavsttd mavsttd Speech-to-text daemon (whisper.cpp / remote worker)
cmd/mavttsd mavttsd Text-to-speech daemon (piper)
cmd/mavgpud mavgpud GPU proxy daemon (workpc inference)
cmd/mavcaldav mavcaldav CalDAV sync daemon
cmd/mavmaild mavmaild Mail ingestion daemon
cmd/mavpoll mavpoll Polling daemon (Kuma monitors)
cmd/mavenclient mavenclient CLI client
cmd/mavupdate mavupdate Self-update tool
cmd/mavseal mavseal Seal/encryption tool
cmd/e2eprobe e2eprobe End-to-end probe
cmd/labelgen labelgen Label generation tool
cmd/mavend/seedtest (test helper) Test seeder

Internal packages relevant to routing/action

Package Key files Role
internal/router router.go, intent.go, stage0.go, stagezero.go, heads.go, llmrouter.go, classifier.go, slots.go, source.go, embedder.go, onnxembedder.go, question.go, singletoken.go, notecapture.go, claim.go (package-level), decisiontrace.go Routing cascade, intent taxonomy, slot extraction, confidence
internal/claim claim.go Band/Claim/arbitration types (imported by router and dialogue)
internal/dialogue session.go, clarify.go, pending.go, pending.go Session state, clarification, PendingAction/Capability
internal/tool tool.go, risk.go Tool execution, risk tiers, policy
internal/phraser phraser.go, llmphraser.go, fallbacks.go, replier.go Response phrasing (LLM or stub)
internal/voice server.go, wire.go, replier.go Voice TCP server, wire protocol
internal/memory store.go Vector memory (cosine similarity search)
internal/llm remote.go Two-tier LLM client (resident + workstation)
internal/lexicon lexicon.go Closed Russian word sets (embedded JSON)
internal/morph morph.go Russian morphology (golem lemmatizer)
internal/loop loop.go Proactive nudge rules, gates
internal/store facts.go, reminders.go, tools.go SQLite persistence
internal/ipc coreapi.go, api.go IPC interface + wire types
internal/modes modes.go Mode inventory (30 routing classes)
internal/mcp manager.go, client.go, allowlist.go MCP tool discovery and execution
internal/smarthome client.go Home Assistant service calls

Key cmd/mavend files (the "glue" layer)

File Responsibility
voice.go runTurn() pipeline, reactiveHandler, replySystem, applyAction
turnroute.go turnRoute memo (computed once per turn)
actions.go actionHandlers dispatch table (7 intents)
actions_act.go actionAct: tool matching, ecosystem interception, execution
actions_fact.go actionFact: write fact + embed + vector insert
actions_reminder.go actionReminder: parse time + create reminder
actions_note.go actionNote: write note + embed + vector insert
actions_query.go actionQuery: 20+ source chain, queryTurn, queryWalk
confirm.go resolveConfirm: y/n for destructive acts, Hexis, routines
clarify.go askClarify, resolveClarifyAnswer, wantedSlots
followup.go followUpMerge: slot inheritance across turns
continuation.go continuationDecision: elliptical follow-ups
boot.go Daemon wiring: connects all pieces
voicewire.go Voice server wiring: STT, router, replier, tools, sessions
tick.go Proactive tick loop: nudge delivery, reminders, routines
tick_routines.go Routine firing, pattern detection
tick_digest.go Digest queue and flush

C. Ingress -> Routing -> Action Call Trace

Text path (web/telegram)

1. mavweb POST /api/chat (cmd/mavweb/chat.go:84)
   -> strings.TrimSpace(r.FormValue("text"))
   -> core.Chat(ctx, "web", text) (ipc/coreapi.go:205-213)

2. daemonAPI.Chat (cmd/mavend/tick_api.go:95)
   -> chatFn(ctx, conversation, text)
   -> handler.handleText(ctx, conversation, text) (voice.go:245)

3. handleText (voice.go:245-248)
   -> runTurn(withDialogueID(ctx, ...), text, sourceText)

4. runTurn (voice.go:270-481) -- see Section A for step-by-step

Voice path (mic)

1. Voice TCP Server receives PushToTalk frame (internal/voice/server.go:187-215)
   -> handler.HandlePushToTalk(ctx, req, sid) (cmd/mavend/voice.go:200-218)

2. HandlePushToTalk:
   -> stt.Transcribe(ctx, req.Audio)  -- whisper.cpp or remote worker
   -> runTurn(ctx, text, sourceVoice)  -- same pipeline as text
   -> tts.Synthesize(ctx, replyText)   -- piper
   -> return voice.PushToTalkResp{ReplyText, ReplyAudio}

runTurn step-by-step (voice.go:270-481)

Step 0 (276): decision.Record installed on context (V-564)
Step 0b (292): turnRoute computed once, shared via context
Step 1 (313): clarifyExpiredNotice -- parked question TTL ran out
Step 2 (318): resolveConfirm -- y/n for parked destructive act
  -> classifyConfirm(text) (confirm.go:264)
  -> confirmResolvers chain (confirm.go:114):
     1. pendingRoutineConfirm
     2. pendingHexisExec
     3. pendingAct -> tools.Exec(ctx, fn, args, true)
Step 3 (327): resolveRepair -- "нет, это был вопрос"
Step 3b (334): resolveUntargetedRepair -- "нет, не так"
Step 3c (344): resolveCommandProhibition -- "не отменяй..."
Step 4 (356): resolveClarifyAnswer -- answer to parked question
Step 5 (366): resolveQuietToggle -- "тихий режим"
Step 5b (374): resolveSnooze -- "не сейчас" / "потом"
Step 5c (381): resolveAck -- "готово"
Step 5d (389): resolveReminderCancellation -- cancel verb + noun
Step 5e (397): resolveCandidate -- "второй" (ordinal)
Step 6 (406): ROUTE
  -> turnRoute.resolve(ctx) (turnroute.go:68)
  -> continuationDecision(prev, text, now) OR router.Route(ctx, text, now)
Step 7 (425): followUpMerge(prev, dec, now) -- slot inheritance
Step 8 (445): clarify -- missing required slots
  -> hexisBeforeClarify -- try Hexis before asking
  -> askClarify -> park PendingQuestion
Step 9 (466): applyAction -> actionHandlers[dec.Intent]
Step 9b (474): ackFromFact -- close live nudge
Step 10 (477): replier -- phrase the reply (LLM or stub)

Action dispatch (voice.go:496-504 -> actions.go:48-56)

applyAction(ctx, dec):
  if dec.Clarify -> return "" (Replier phrases)
  actionHandlers[dec.Intent](h, ctx, dec):

  IntentFact     -> actionFact (actions_fact.go:17)
    -> coreAPI.WriteFact + memStore.Insert + detectPattern

  IntentReminder -> actionReminder (actions_reminder.go:16)
    -> coreAPI.CreateReminder

  IntentAct      -> actionAct (actions_act.go:17)
    -> refusesCommand check
    -> matcher.Match (fuzzy prefix over enabled tool names)
    -> task_status intercept
    -> praxis intercept
    -> hexis intercept (resolve entity -> discover capabilities -> risk -> exec)
    -> proposeGap (if no match)
    -> tools.Exec (tool/tool.go:156):
       LookupTool -> RiskOf -> PolicyFor(tier) -> dispatch:
         MCP -> mcp.CallPositional
         HA -> smarthome.CallService
         Process -> exec.CommandContext

  IntentChat     -> actionChat (actions.go:58)
    -> phraser.PhraseChat(ctx, utterance, history)

  IntentSystem   -> actionSystem (actions.go:77)
    -> replySystem: keyword match on utterance

  IntentNote     -> actionNote (actions_note.go:24)
    -> coreAPI.WriteNote + memStore.Insert

  IntentQuery    -> actionQuery (actions_query.go:varies)
    -> querySources chain (20+ sources, first claim wins)
    -> queryWalk narrows by destination

D. Inventory of Existing Machinery

D1. Deterministic fast-path recognizers

Status: EXISTS, extensive, production-critical

Component File:line What it does
22 stage-0 grammars router/stagezero.go:24-91 Ordered regex/structural rules. First match wins at confidence 1.0
Grammar type router/stage0.go:20-29 Pattern+Build (regex) or Decide (structural)
Wake-word strip router/stage0.go:66-78 StripWakeToken: removes "мавен" in any script
Act allowlist fast path router/stage0.go:55,80 "мавен, restart nginx" -> act at stage 0
Command prohibition stagezero.go:30 "don't restart nginx" -> refusal sentinel
System time/date stagezero.go:35 "сколько времени", "который час"
Agenda query stagezero.go:39 "что у меня сегодня"
Reminder stagezero.go:59 "напомни через час" with time extraction
Fact capture stagezero.go:80 "запиши купить молоко" with note body extraction
Possession statement stagezero.go:89 "у меня кончилась вода"
Lexicon (closed word sets) lexicon/lexicon.go ~40 embedded Russian word sets
Morphology morph/morph.go Lemma(), IsVerbForm(), SameWord() via golem
Question detection router/question.go IsQuestionShaped(), IsOpenQuestionShaped(), CarriesCaptureVerb()
Single-token analysis router/singletoken.go thinSingleToken() with completeSingles escape
Note capture parser router/notecapture.go ParseNoteCapture(): strips capture frame
DateTimeParser router/slots.go:19-21 Interface; production is dateparser (not shown)
DefaultFactParser router/slots.go:154-180 Lemma-based fact extraction (water/meal/shower/break/sleep)
DefaultActMatcher router/slots.go:106-149 Exact phrase prefix + aliases, longest-first

D2. Learned routing/NLU

Status: EXISTS, multi-layered

Component File:line What it does
RouterHeads (ONNX) router/heads.go:55-64 4 heads over e5-small: intent, destination, slot BIO, clarify
RouterHeads.Route router/heads.go:120+ Softmax classification, threshold=0.6, single forward pass
LLMRouter router/llmrouter.go:20-22 GBNF-constrained JSON from Qwen3-1.7B
LLMRouter.Route router/llmrouter.go:120+ System prompt + grammar -> routeAction structs
gateLLMDecision router/router.go:352-373 Thin confidence (0.3) for incomplete slots
Classifier router/classifier.go:40-45 Nearest-centroid over ONNX embeddings
Classifier.Classify router/classifier.go:50+ Cosine similarity, sorted best-first
Classifier.AddExample router/classifier.go Append-only correction (grows classifier)
ONNX Embedder router/onnxembedder.go multilingual-e5-small, 384-dim, query/passage prefix
HashEmbedder router/embedder.go:79-116 Fallback bag-of-words (deterministic, weak)
Embedder interface router/embedder.go:17-21 Dim(), Embed(), Close()
Intent taxonomy router/intent.go:38-63 7 intents: act, reminder, fact, note, query, chat, system
Source taxonomy router/source.go 12 destinations: recall, calendar, tasks, list, money, weather, home, network, feeds, attention, self, world
Slots struct router/intent.go:66-94 Time, Fn, Args, Key, Value, Text + Has* flags
Decision struct router/intent.go:100-128 Utterance, Stage, Intent, Confidence, Slots, Clarify, Source, SourceAnchored
Modes inventory modes/modes.go ~30 distinct downstream behaviors, embedded JSON
Confidence gate router/router.go:244-248 threshold=0.55, below -> Clarify=true

D3. Claim/arbitration system

Status: EXISTS, not wired into cascade (per claim.go:138-140)

Component File:line What it does
Band enum claim/claim.go:38-69 BandUnknown, BandVetoed, BandNearest, BandStructural, BandAnchored
Claim struct claim/claim.go:88-117 Claimant, Intent, Filled, Consumed, Unexplained, Band, Veto
Coverage() claim/claim.go:122-128 Consumed / (Consumed + Unexplained)
MoreSpecificThan() claim/claim.go:141-147 Coverage first, Band breaks ties
Tokens() claim/claim.go:156-169 Tokenize utterance for coverage
Split() claim/claim.go:178-192 Partition tokens into consumed/unexplained

Note: claim.go:138-140 explicitly states this is "Deliberately NOT wired into the cascade by V-565." It is here so the ordering is one function with tests rather than duplicated logic.

D4. Action path

Status: EXISTS, with multiple dispatch paths

Component File:line What it does
actionHandlers table actions.go:48-56 7-intent dispatch map
applyAction voice.go:496-504 Short-circuits on Clarify, dispatches via table
actionAct actions_act.go:17-114 Full act cascade: refuse -> match -> task_status -> praxis -> hexis -> propose -> exec
tool.Executor.Exec tool/tool.go:156-235 LookupTool -> RiskOf -> PolicyFor -> dispatch (MCP/HA/process)
RiskOf tool/risk.go Derives tier from Tool row
PolicyFor tool/risk.go TierSafe/TierDestructive/TierIrreversible -> Confirm/VoiceMayRun
Matcher.Match tool/tool.go:251+ (tool package) Fuzzy prefix over enabled tool names
MCP dispatch tool/tool.go:191-198 mcp.ParseCmd -> mcp.CallPositional
HA dispatch tool/tool.go:205-221 smarthome.ParseCmd -> smarthome.CallService
Process dispatch tool/tool.go:231-234 exec.CommandContext (no shell)
Hexis integration ecosystem_acts.go:660+ resolve entity -> discover capabilities -> risk -> confirm/exec
Praxis integration ecosystem_acts.go (handlePraxisAct) Attention/item lifecycle
UnknownTargetError tool/tool.go:90-96 Named error with target word
Tool store store/tools.go ProposeTool, EnableTool, DisableTool, LookupTool, ReconcileMCPTool

D5. Confirmation/risk handling

Status: EXISTS, comprehensive

Component File:line What it does
resolveConfirm confirm.go:78-100 classifyConfirm + chain of resolvers
classifyConfirm confirm.go:264+ Closed yes/no lexicon, entire utterance must match
confirmResolvers confirm.go:114-186 3 slots: routine proposal, Hexis exec, local tool
confirmTTL confirm.go:59 90s
pendingAct confirm.go:48-54 fn, args, phrase, expiry
pendingHexisExec confirm.go:21-35 capabilityID, entityID, correlationID, expiry
pendingRoutineConfirm confirm.go:39-46 routineID, action, object, interval, phrase, expiry
park() confirm.go:63-67 Stores pendingAct

D6. Non-action paths

Component File:line What it does
actionChat actions.go:58-75 phraser.PhraseChat with dialogue history
replySystem voice.go:509-569 Keyword matching on utterance for time/date/status
actionQuery chain actions_query.go:99-188 20+ sources, first claim wins
queryWalk actions_query.go:190+ Narrows chain by destination
Best recall recall.go:8-65 Vector search with confidence gate (minScore + minMargin)
Clarify store dialogue/clarify.go PendingQuestion stack (max depth 2), 90s TTL
Follow-up merge followup.go:99-194 Slot inheritance across same-intent turns
Continuation continuation.go:57-98 Elliptical follow-ups ("а завтра?")
Tick loop tick.go Proactive nudge delivery, reminders, routines, patterns, digest

D7. Tests/data inventory

Status: Extensive (354 test files)

Key test fixtures:

Fixture File Cases
Routing contract internal/router/eval/ru_routing_v1.json ~70 held-out utterances with intent/source/time/clarity expectations
Ecosystem reach internal/router/eval/ru_ecosystem_v1.json ~50 act utterances with service/capability expectations
Nudge phrasing internal/phraser/eval/nudges_v1.json 15 cases, on-topic + property checks
Talk phrasing internal/phraser/eval/talk_v1.json 27 cases (chat/query/knowledge)
Recall contract internal/memory/recalleval/ru_recall_v1.json ~40 cases with note sets
Personal boundary cmd/mavend/testdata/personal_boundary_v1.json 72 cases
Safety scenarios cmd/mavend/testdata/system_safety_scenarios.json 4 scenarios
Simulator scenarios cmd/mavend/testdata/scenarios/*.json 5 full scripted scenarios
Usage transcript scripts/testdata/usage-turns.txt 167-line simulated usage
STT golden cmd/mavsttd/testdata/golden_v1.json 4 cases with WER bounds

Key eval harnesses:

Harness File
Routing eval internal/router/eval/eval.go
Heads eval internal/router/eval/heads_test.go
LLM router eval internal/router/eval/llmrouter_test.go
Ecosystem reach eval internal/router/eval/reach_test.go
Claim scoring internal/router/eval/claims_test.go
Nudge phrasing eval internal/phraser/eval/eval.go
Talk phrasing eval internal/phraser/eval/talk.go
Recall eval internal/memory/recalleval/recalleval.go
Kiwix rewrite eval internal/kiwix/rewrite_eval.go
Simulator cmd/mavend/simulator_test.go

Classifier seed phrases: models/seeds/{query,fact,chat,act,note,reminder,system}.txt


E. Existing Contracts/Types We Can Reuse

Already well-typed (reuse as-is or thin wrapper)

Type File:line Notes
router.Intent intent.go:38 String enum: act, reminder, fact, note, query, chat, system
router.Slots intent.go:66-94 Typed: Time, Fn, Args, Key, Value, Text + Has* flags
router.Decision intent.go:100-128 Utterance, Stage, Intent, Confidence, Slots, Clarify, Source, SourceAnchored
router.Source source.go String enum: 12 destinations
router.Grammar stage0.go:20-29 Pattern+Build or Decide
router.Extractor slots.go:45-49 Time, Acts, Facts parsers
claim.Claim claim.go:88-117 Band-based arbitration (not wired yet)
claim.Band claim.go:36-69 Ordinal evidence kinds
dialogue.PendingAction pending.go:58 Capability, slots, missing, utterance, TTL
dialogue.Capability pending.go:16-46 7 capability strings
tool.Executor tool/tool.go:116-122 Exec with policy checks
tool.Policy tool/risk.go Confirm, VoiceMayRun per tier
ipc.ChatReply ipc/api.go:752-761 Reply, Source, TraceID
ipc.Tool ipc/api.go:699 Wire shape of tool row

Partially exists (needs extension)

Concept Current form Gap
NormalizedInput Raw string in, text string parameter No NormalizedInput struct; STT output passes as-is
FastPathResult Stage-0 grammar Decision Not a separate type; embedded in Decision
RouteDecision router.Decision Already carries Stage (0/1/2/3), Intent, Confidence, Slots. Could become RouteDecision
ActionCandidate router.Decision + actionHandlers dispatch No explicit ActionCandidate type; intent + slots + handler selection are implicit

Missing (must be created if needed)

Concept Notes
Schema validation No JSON-schema or struct validation on incoming slots before execution
Confidence/risk policy Risk tiers exist for tools but not for routing confidence. Stage 3 gate exists but is a simple threshold
Post-execution verification No explicit verification step after tool execution (success/failure is the extent)

F. Gaps Against the Proposed First-Stage Design

Proposed pipeline vs current reality

Proposed stage Current state Gap
NormalizedInput Raw text string everywhere No normalization struct. Preprocessing is scattered: StripWakeToken in router, lowercase in matchers, trim in entry points
Deterministic fast-path Stage-0 grammars (22 rules) EXISTS and is production-critical. However: grammars produce Decision directly, not a separate FastPathResult type. No schema validation on slots before returning
Learned routing Heads -> LLM -> Classifier cascade EXISTS and complex. But: multiple confidence scales (stage-0=1.0, heads=softmax, LLM=1.0/0.3, classifier=cosine). No unified confidence model
RouteDecision router.Decision EXISTS under another name. Carries Stage, Intent, Confidence, Slots, Clarify, Source. Could be wrapped/renamed
ActionCandidate Implicit in router.Decision + actionHandlers Missing as explicit type. The decision arrives at applyAction and is dispatched by intent. No schema validation of slots before dispatch
Schema validation NONE Slots are filled by extractors and used directly. No validation that e.g. reminder has both Text and Time before actionReminder runs
Confidence/risk policy Threshold gate (0.55) for routing; risk tiers for tools No unified confidence policy. Routing confidence and tool risk are separate systems. No policy that says "if confidence < X, require confirmation for action"
Confirmation resolveConfirm with 3 pending slots EXISTS for destructive tools, Hexis, and routines. Not applied to routing confidence (a low-confidence act just gets proposed, not confirmed)
Execution tool.Executor.Exec EXISTS with MCP/HA/process dispatch. Well-structured. But: actionFact, actionReminder, actionNote bypass tool.Executor entirely (they call CoreAPI directly)
Post-execution verification Success/failure error handling in actionAct Partially EXISTS. Tool execution returns (out, err). Error types are handled specifically. But: no structured verification step, no retry policy, no rollback

Architectural problems identified

  1. Fast paths that execute directly: actionFact, actionReminder, actionNote call CoreAPI.WriteFact/CreateReminder/WriteNote directly from the action handler, bypassing tool.Executor. This means they skip the risk tier system, the confirm gate, and the allowlist. This is by design (facts/reminders are user-stated, not tool invocations) but means the "all actions through one pipeline" goal requires either wrapping these in tool-like abstractions or explicitly exempting them.

  2. Multiple confidence scales: Stage-0 = 1.0 (hardcode), heads = softmax float, LLM = 1.0 or 0.3 (thin), classifier = cosine similarity. These are not on the same scale and cannot be compared. The claim system (claim.Band) explicitly addresses this by making confidence ordinal (Band) rather than graded. The proposed design should preserve this insight.

  3. Routing code that also performs tool selection: actionAct at actions_act.go:29-33 runs the matcher inline when HasFn is false. The matcher is also the stage-2 ActMatcher. So tool selection happens both in the router (stage 2) and in the action handler (actionAct). The actionAct path is the fallback for LLM-routed acts where the verb didn't go through stage-0.

  4. Implicit fallthrough: The router cascade is explicitly designed as fallthrough (each stage may decline). The query source chain is also fallthrough (first claim wins). The confirm resolver chain is also fallthrough. This is a consistent pattern, not a bug, but means the "stage-to-stage" architecture must preserve explicit decline semantics.

  5. Clarify bypasses action pipeline: When dec.Clarify is true, applyAction returns "" immediately (voice.go:497-499). The clarifier can also call hexisBeforeClarify (voice.go:446) to try Hexis before asking, which is a hidden action path that runs before the normal action dispatch.

  6. No schema validation: Slots filled by stage-2 extraction or stage-0 grammars are used directly by action handlers. actionReminder (actions_reminder.go) checks HasTime itself. actionFact checks HasKey. But there is no shared validation layer; each handler does its own checks.

  7. Voice-specific behavior: HandlePushToTalk wraps runTurn with STT before and TTS after. The turnSource tag ("tap:voice" vs "tap:text") propagates into fact sources but does not change routing or action behavior. However: the voice path has barge-in, session management, and wake-word detection that the text path lacks entirely. The semantic behavior is the same; the infrastructure is different.

  8. Existing useful code to preserve:

    • Stage-0 grammars: 22 ordered rules, battle-tested, each with extensive comments about why it sits where it sits. Moving or reordering them breaks routing.
    • The claim/Band system: explicitly designed for the problem of incomparable confidence scales. Not wired yet but well-tested.
    • The clarify store with stack support: handles nested clarification flows.
    • The query source chain with destination narrowing: 20+ sources with guessers vs lookups distinction.
    • The tool risk tier system: well-tested, with voice-specific authority limits.
    • The turnRoute memo pattern (V-560): computed once, shared via context, prevents routing disagreement.

G. Smallest Behavior-Preserving Refactor Boundary

The smallest refactor that aligns with the proposed architecture without changing behavior:

Wrap Decision in RouteDecision + add NormalizedInput as thin alias

Current:  router.Route(ctx, utterance, now) -> (Decision, error)
Proposed: router.Route(ctx, NormalizedInput, now) -> (RouteDecision, error)

Where:

  • NormalizedInput is type NormalizedInput struct { Text string; Source string } -- a thin wrapper, not a transformation
  • RouteDecision is type RouteDecision Decision -- or just Decision with a type alias
  • The existing Stage field (0/1/2/3) already encodes which stage produced the result
  • The existing Confidence field already carries the per-stage confidence

This changes zero behavior. It names what exists. It creates the typed boundary the future stages need.

Second step: extract action candidates

Currently actionAct does tool matching inline. The matcher result (fn, args) should be a typed ActionCandidate returned by the router or by a post-route step, not discovered inside the action handler. But this changes the call structure of actionAct, which is a larger refactor.

Third step: schema validation

Add a Validate(slots) step between routing and action dispatch. Currently each handler validates its own slots; this would centralize it. Minimal behavior change: the same checks, in one place.


  1. Type the boundaries (1-2 hours)

    • Define NormalizedInput, RouteDecision as thin wrappers
    • Route() signature change (internal callers only)
    • Zero behavior change
  2. Pin current behavior with regression tests (2-3 hours)

    • Run existing eval fixtures and record baselines
    • Add integration tests for the full runTurn pipeline (text + voice paths)
    • Add tests for each action handler with representative inputs
  3. Extract ActionCandidate from actionAct (3-4 hours)

    • Move tool matching out of actionAct into a post-route step
    • Return ActionCandidate{Fn, Args, Source} from routing
    • actionAct consumes ActionCandidate instead of re-matching
  4. Add schema validation layer (2-3 hours)

    • Validate slots before action dispatch
    • Centralize the per-handler checks
    • Fail-closed: missing required slot -> clarify, not runtime error
  5. Unify confidence presentation (3-4 hours)

    • Map per-stage confidence to ordinal Band (leverage existing claim.Band)
    • Expose in RouteDecision for downstream policy
    • Do NOT try to make confidence comparable across stages
  6. Wire claim.Band into cascade (4-6 hours)

    • Replace ad-hoc precedence with MoreSpecificThan
    • This is the V-558/V-565 work already planned

I. Tests That Should Pin Current Behavior Before Refactoring

High-value regression pins

Test What it pins File
Router cascade stage ordering Stage-0 wins, heads decline correctly, LLM fallback, classifier floor internal/router/router_test.go
Held-out routing contract ~70 utterances with intent/source/time expectations internal/router/eval/eval_test.go
Ecosystem reach contract ~50 act utterances routing to correct service internal/router/eval/reach_test.go
Simulator scripted day Full pipeline: STT -> router -> store -> phraser cmd/mavend/simulator_test.go
Safety scenarios Destructive acts require confirmation, ambiguous entities clarified cmd/mavend/eval_scenarios_test.go
Tool risk assessment Tier derivation from tool rows internal/tool/ risk_test.go (implied)
Confirm flow y/n for parked acts, TTL expiry, chain ordering cmd/mavend/confirm_test.go
Clarify flow Missing slots -> question -> answer -> continue cmd/mavend/clarify_test.go
Follow-up merge Slot inheritance across turns cmd/mavend/followup_test.go
Query source chain First-claim-wins, destination narrowing cmd/mavend/querywalk_test.go
Personal boundary 72-case held-out fixture cmd/mavend/personalboundary_test.go
Recall contract ~40 cases with paraphrased queries internal/memory/recalleval/recalleval_test.go
Action act risk Destructive/irreversible classification cmd/mavend/actions_act_risk_test.go
Degradation Each ecosystem service unreachable cmd/mavend/ecosystem_degraded_test.go

What to run before and after each refactor step

make test           # full suite
go test ./internal/router/eval/ -run Eval  # routing contract
go test ./cmd/mavend/ -run Simulator       # integration
go test ./cmd/mavend/ -run Eval            # safety scenarios
go test ./cmd/mavend/ -run PersonalBoundary # boundary fixture
go test ./internal/phraser/eval/ -run Eval  # phrasing contract
go test ./internal/memory/recalleval/ -run Eval # recall contract

J. Unknowns That Cannot Be Established From Code/Tests

  1. Actual production accuracy numbers: The eval fixtures measure held-out accuracy, but production routing traces (routing_traces table) are the real measure. We cannot inspect the production DB from code.

  2. Whether the LLM router is currently enabled in production: The config shows llm_router settings but we cannot confirm the daemon is running with it wired. The heads may be the actual fast path.

  3. Real-world confirmation rates: How often do users get asked to confirm? How often do they decline? This is behavioral data, not code.

  4. Whether the claim system should be wired: claim.go says "Deliberately NOT wired by V-565" but the current ad-hoc precedence works. The claim system is tested but untested in production.

  5. Token budget pressure on the resident model: The 4096 context window is shared between routing, phraser, and chat. We cannot tell from code whether context pressure causes routing failures in production.

  6. Whether stage-0 grammars overlap or shadow each other: The ordering is documented, but no test measures "if grammar A were removed, which utterances would fall through differently." The cascade hides contention by design.

  7. Performance characteristics of the ONNX embedder in production: Tests measure p50 (20.6ms for classifier). Production numbers on the actual hardware may differ.

  8. Whether the ecology of pre-route resolvers (steps 1-5e) can be unified: Seven resolvers each claim the turn independently, in order. Whether they could be replaced by a single arbiter (the claim system) is a design question, not a code question.


Proposed Mapping: Future Stage/Contract -> Current Implementation

Future stage/contract Current implementation Reuse/wrap/move/replace Reason
NormalizedInput Raw text string parameter in handleText/HandlePushToTalk/runTurn wrap Create struct, pass through. No transformation needed yet. Existing preprocessing (StripWakeToken, trim) stays inside the router.
Deterministic fast path Stage-0 grammars (router/stagezero.go, stage0.go) reuse as-is 22 battle-tested rules with load-bearing ordering. Output is Decision at confidence 1.0. Naming it "fast path" is cosmetic.
FastPathResult Decision with Stage=0 wrap Type alias or thin struct. The Stage field already identifies the source.
RouteDecision router.Decision (intent.go:100-128) reuse (rename or alias) Already carries all needed fields: Intent, Confidence, Slots, Clarify, Source, Stage. The Stage field (0/1/2/3) tells which cascade stage produced it.
ActionCandidate Implicit: Decision.Intent + Decision.Slots + actionHandlers dispatch move Extract from actionAct into a post-route step. Currently, actionAct:29-33 re-runs the matcher when HasFn is false. This should produce an ActionCandidate that actionAct consumes.
Schema validation Per-handler checks (actionReminder checks HasTime, actionFact checks HasKey) move + centralize Currently scattered across action handlers. Centralize into a Validate(Decision) step before applyAction.
Confidence/risk policy Stage-3 gate (threshold 0.55) for routing; RiskOf/PolicyFor for tools extend These are separate systems today. The routing gate Clarify flag. The tool policy returns ErrNeedsConfirm/ErrNeedsAuthedSurface. A unified policy would map confidence bands to action policies.
Confirmation resolveConfirm (confirm.go:78) with 3 pending slots reuse Already handles destructive tools, Hexis, and routines. Would need extension if low-confidence acts should also confirm.
Execution tool.Executor.Exec (tool/tool.go:156) + per-intent handlers reuse Well-structured with MCP/HA/process dispatch. The per-intent handlers (actionFact, actionReminder) bypass Executor by design -- they write to the store, not run tools.
Post-execution verification Error handling in actionAct (actions_act.go:64-108) extend Currently: success -> "done", specific error -> specific reply. No structured verification step. Adding one would be a new layer.