feat(recall): wire neural embeddings into the live persona path (#40) - #1715
Merged
Conversation
The live persona recall path (build_workspace_cycle → RecallFaculty) hard-coded
the lexical bootstrap embedder. Now it uses neural embeddings (via unsloth's
/v1/embeddings, landed last commit) when the embed model actually serves, and
falls back to lexical otherwise — so personas get semantic recall, not just
word-overlap, the moment an embed model is available.
resolve_recall_embedder(adapter):
- Prefers NeuralEmbeddingProvider (semantic) but PROBES it once with a one-shot
embed. A usable probe (non-empty, non-zero, all-finite) → neural; an empty /
zero / NaN probe (model not loaded, endpoint error) → lexical fallback. This
is the no-signal guard: without it, an adapter that advertises embedding
support but has no embed model loaded would embed every memory into a zero
vector and recall would return nothing — strictly worse than lexical.
- The choice is PROCESS-STABLE (decided once at spawn, never per-embed): a query
and the stored vectors must live in the SAME embedding space, so neural and
lexical are never mixed per call (cosine across spaces is meaningless).
- Result is wrapped in the content-addressed CachingEmbeddingProvider — each
message embedded ONCE and shared across every persona (the latency win; the
embed model slug is the cache/space key). Further perf is a later pass.
- Always returns a working embedder — never errors/panics. A box with no embed
model still gets real lexical relevance ("solve for public users").
Wiring:
- PersonaBrainConfig gains `embedder: Option<Arc<dyn EmbeddingProvider>>`;
build_workspace_cycle uses it, defaulting to lexical+cache when None (harnesses).
- The live supervisor spawn sets it via resolve_recall_embedder(adapter).
- CANONICAL_EMBED_MODEL = "qwen3-embedding-0.6b" (dim 1024), overridable via
UNSLOTH_EMBED_MODEL — one embedding space across the grid.
7 new tests: the probe gate (usable vs no-signal), and the resolver picking
neural when the model serves / falling back to lexical on empty-probe / using
lexical when the adapter lacks embedding support.
Scope: this does NOT delete fastembed — that path (the separate IPC Hippocampus
/ PersonaMemoryManager, with real blast radius into ORM vector + search) is the
follow-up trim, deferred per "get it working first, optimize once healthy."
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The live persona recall path (
build_workspace_cycle→RecallFaculty)hard-coded the lexical bootstrap embedder. Now it uses neural embeddings (via
unsloth's
/v1/embeddings, #1714) when the embed model actually serves, andfalls back to lexical otherwise — personas get semantic recall, not just
word-overlap, the moment an embed model is available. Builds directly on
#1713 (startup auto-fuel) + #1714 (
create_embedding).resolve_recall_embedder(adapter)(non-empty, non-zero, all-finite) → neural; empty/zero/NaN probe (model not
loaded, endpoint error) → lexical fallback. This is the no-signal guard:
without it, an adapter that advertises embedding support but has no embed model
loaded would embed every memory into a zero vector and recall would return
nothing — strictly worse than lexical.
stored vectors must share one embedding space, so neural and lexical are never
mixed per call (cosine across spaces is meaningless).
CachingEmbeddingProvider— each messageembedded once and shared across every persona (the latency win). Further
perf is a later pass.
embed model still gets real lexical relevance ("solve for public users").
Wiring
PersonaBrainConfiggainsembedder: Option<Arc<dyn EmbeddingProvider>>;build_workspace_cycleuses it, defaulting to lexical+cache whenNone.resolve_recall_embedder(adapter).CANONICAL_EMBED_MODEL = "qwen3-embedding-0.6b"(dim 1024), overridable viaUNSLOTH_EMBED_MODEL— one embedding space across the grid.Tests
7 new: the probe gate (usable vs no-signal), and the resolver picking neural when
the model serves / falling back on empty-probe / using lexical when the adapter
lacks embedding support.
Scope
Does not delete fastembed — that path (the separate IPC Hippocampus /
PersonaMemoryManager, real blast radius into ORM vector + search) is thefollow-up trim, deferred per "get it working first, optimize once healthy."
🤖 Generated with Claude Code