From 7138086c3fcc28704e0eae2b9adee2a51b4f7d1a Mon Sep 17 00:00:00 2001 From: claude Date: Sat, 8 Aug 2026 22:33:35 +0400 Subject: [PATCH] The routing heads run in Go now, so say so (V-664) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN --- CLAUDE.md | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fc4fe84..558f6e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -308,9 +308,36 @@ the possessive agenda rules claim those cases at stage 0 and name nothing, so no label reaches the head. That is the same trade V-660 flagged and it wants the owner's call. -**Nothing of this runs in Go.** The weights are `heads.pt` and `out/body_heads/` -on workpc. Reaching the daemon needs an ONNX export and a caller. The resident -e5-small must not be replaced by the copy, because recall depends on that file. +**The heads run in Go and route every turn, since 08-08-2026** (V-664, +`docs/evals/2026-08-08-routing-heads-in-go.md`). This section used to say +nothing of it ran. `RouterHeads` in `internal/router/heads.go` loads +`router_heads.onnx` and reads intent, destination and clarify off one forward +pass. It is stage 0b: after the grammars, **before** the resident model, and the +classifier is still behind both. Through the cascade it scores intent **96.9%** +and destination **75.8%** at p50 27.9ms. That beats the gemma-4-12b cascade, +84.4% and 72.7%, at a twelfth of its 329ms. The workstation stays the better +phraser and is no longer the better router. + +Three rules around it. The **clarify head decides first**, before the intent +threshold. It answers a different question. A thin utterance scores low +intent by construction, so gating it cost 6 of 8 ambiguous cases. The +**destination head is read on `IntentQuery` only**, since no other intent +reaches `queryWalk`. And `headsThreshold` is 0.6, the measured knee: every value +to 0.85 drops right answers and keeps the same two wrong ones. + +`voice.embedder.heads_path` is the whole switch. Empty, missing or unloadable +means the heads are nil and the cascade is byte-for-byte what shipped before +them. **It must never be pointed at `model_path`.** The resident e5-small must +not be replaced by the fine-tuned copy. Recall depends on that file scoring +what it scored. + +**The hand-written tokenizer read every long word backwards** until this task +(`encodeWord`, `onnxembedder.go`). It cost recall@1 7.4 points and recall@3 11.1. +Nothing caught it because seeds and queries were mangled the same way, so cosine +survived. The heads found it. They are trained through transformers and read +through this. The embedder id now carries a tokenizer revision +(`@384/tok2`), so fixing the tokenizer triggers `ReembedAll` the way swapping the +model file does. Bump `tokenizerRev` on any change to what it emits. `Confidence: 1.0` used to be hardcoded in `llmrouter.go`, so the LLM path could never ask for clarification (6/6 refusal cases missed on the fixture) — Vikunja