Viska · Mímir · Engineering Research Synthesis

How Mímir pulls from many knowledge sources, reasons over them, and never has a blindspot

"Everything lives in Supabase tables — how do we make it so Mímir easily pulls information from different sources, reasons about them, always knows where to look, and doesn't have blindspots?"
Date · 2026-06-28 Room · wr-viska-main Method · 4-lane parallel research team Spec · SPEC-knowledge-architecture-multi-source-routing.md Tier · decision-grade
The short version

One paragraph, then the detail

The whole idea

Give Mímir a source registry that lists every Supabase knowledge store the way the module system already lists capabilities, and put a one-line menu of it in front of the model every turn. Make the default action for any knowledge question a single tool that fans out across the whole registry — so coverage is guaranteed by registry membership, not by the model remembering. A CI test makes any unregistered table a build break. Merge the mixed results with rank-fusion then one rerank pass so a tweet, a research chunk, and a database row can finally be compared. Carry a provenance tag on every result so every answer cites its source and a human can click through. And fix the conversation memory that is currently written but never read. New sources — including the X-post analysis shards — join all of it by dropping one file into the registry.

Part 1 — The diagnosis

Why Mímir has blindspots today

The single live chat path (mention-mastra.js) injects no knowledge up front — only today's date. The large language model sees nothing from Supabase unless it chooses to call a tool. So "which sources get consulted" is an unbounded guess by the model, with no menu and no safety net. That guess leaks. Here is exactly what falls through:

Root cause: coverage is left to the model's memory instead of being a property of the system. The entire fix is to make coverage structural — something the architecture guarantees, not something we hope the model remembers.
Part 2 — The architecture in one picture

From a Slack question to a cited answer

Five components, color-coded to the research lane that designed each. Read it top to bottom — it's the life of a single question.

@Mímir  ─►  mention-mastra.js(1) cheap pre-parse: detect query MODE + extract filters (date / org / entity)
              │  (2) the SOURCE-CATALOG DIGEST rides in the system prompt  ──┐ the router's menu
              ▼        one line per source:  title · kind · what-it-answers · what-it's-NOT-for
        gpt-5.2 agentic loop   ◄── KEEP it, strengthen it, don't replace it
              │
              ├─ utility ask ("BTC price?") ───────►  route to ONE tool        (unchanged)
              │
              └─ knowledge ask ──►  search_knowledge(query, filters)   ◄── the recall-critical default
                     │      fans out across EVERY registry source whose "answers" match
                     │      corpus · entity-posts(X/OKF) · sector-data · reports · memory · …
                     ▼
              each source runs its own search, over-fetching ~20 candidates
                     ▼
        FUSION   Stage 1 · rank-fusion across sources (database rows pinned)
                  Stage 2 · ONE rerank pass — the moment mixed sources become comparable
                     ▼
        SAFETY NET   coverage audit ("did I check every eligible source?")
                     + weak-result fallback → web search
                     ▼
        synthesis over TYPED structure + provenance  ─►  Slack answer WITH citations
    
Part 3 — The five components, unpacked

What each piece actually does

This is the part that was too condensed in the spec. Each component below has a plain-English job and a concrete example.

1The Source Registry

The job: a single list of every knowledge store Mímir has, where each entry is a small "label" describing the source — what it answers, where it lives in Supabase, how to query it, how fresh it is, how to cite it. It is the exact twin of the module registry Mímir already ships and trusts.

Think of it as: a Module is a verb (a capability); a Source is a noun (a place knowledge lives). Mímir already has the verb list. This adds the noun list, reusing the same proven machinery.

Example label — research-corpus: answers "what analysts say about a ticker/sector"; not for "real-time chatter"; lives in embeddings_main; cite as "{org} — {title} ({date})".

2The Always-On Menu

The job: a compact, one-line-per-source rendering of the registry is placed in the system prompt on every turn. The model can't forget a source exists if it's reminded of all of them, every time.

When the model picks a source, a second tool — describe_source(id) — hands it the full query details on demand, so the prompt stays small even as sources grow.

Before: "the model didn't know mimir_reports was queryable." After: impossible — it's on the menu every turn.

3The Fan-Out Tool

The job: for any knowledge question, the default action is one tool — search_knowledge() — that automatically queries every registry source whose description matches the question, in parallel.

This is the key move: coverage stops being "did the model remember source X?" and becomes "is source X in the registry?" — a membership fact, not a judgment. Add a source to the list and it's instantly part of the coverage guarantee, for free.

Example — "latest on copper" hits corpus + sector-data + tracked-account posts + prior reports + conversation memory in one shot, not whichever one the model happened to choose.

4Fusion + Rerank

The problem: a research chunk scores by similarity (0.82), a tweet by recency, a database row is an exact match. These numbers cannot be compared — you can't sort them into one list.

The fix: two stages. First, rank-fusion combines the lists by position, not score (sidestepping the comparison problem). Then one rerank pass reads the actual text against the question and produces a genuinely comparable relevance score. Database rows that exactly answer the question are pinned, not ranked.

Good news: the rerank code already exists (dead legacy) and is not a new dependency — it's a plain web call. Reviving it is lifting ~80 lines + one API key, with a graceful fallback if the service is down.

5Provenance — "always knows where to look"

The job: every retrieved fact carries a small provenance tag from the moment it's fetched — a stable anchor, a clickable source link, and a pre-rendered citation. Fusion and reranking can reorder or drop results but never alter the tag. At the end, each claim in Mímir's answer maps back to a clickable Slack citation. A fact with no resolvable source renders as plain citation text — never a fabricated link.

Example chain — a number in a sector score → the driving sentence (top_drivers already stores this) → the source document → a Dropbox link. The X-post shard cites the tweet; the tweet's analysis cites the underlying report. Two-level provenance.
Part 4 — The operator's core demand

How "no blindspots" becomes a guarantee, not a hope

Coverage is defended at four independent layers. No single layer is trusted alone — if one slips, the next catches it.

LAYER 1

Default breadth

Knowledge questions fan out to all matching sources by default. Route-to-one is the exception, reserved for unambiguous utility asks.

LAYER 2

Menu visibility

The full source menu is in the prompt every turn. The model can't omit what it's continuously shown.

LAYER 3

Registry invariant

The fan-out is driven by registry membership in code. A CI test makes an unregistered table a build break.

LAYER 4

Audit + fallback

After retrieval, a check computes "every eligible source minus those actually queried." Non-empty → retry. Weak results → web search.

The operator's exact question — "did I check every source that could answer this?" — becomes a computed set difference, logged on every query. That same log makes "no blindspots" a measurable production metric, not an assertion.
Part 5 — The fusion pipeline, step by step

Making a tweet, a research chunk, and a database row comparable

Each source returns ~20 candidates

Over-fetch on purpose. Pull more than you need from each source so the ranking stages have room to work. (The live path regressed to 10; the dead code already knew 20 was better.)

Rank-fusion across sources

Combine the lists by rank position, not raw score, using reciprocal-rank-fusion. This is the whole trick: positions are comparable even when scores aren't. Each source gets a small priority weight. Exact database rows skip ranking entirely — they're pinned as ground truth.

One rerank pass — the unifier

Take the top ~20 fused results and run a single cross-encoder rerank that reads (question, text) together. Because it never sees which source a result came from — only the text — it produces one relevance score that's genuinely comparable across all sources. Hand it the structured shard as JSON and it ranks on the fields, not just prose.

Top results → synthesis, with provenance intact

The final 8 or so, each still carrying its source tag, go to the model. The model writes the answer and cites each claim; the citations render as clickable Slack links.

Part 6 — Two special pieces

The memory fix, and why OKF structure matters

Fixing the write-only memory

The data is already there — every conversation, already embedded. It just has no reader. Ship now: add one database function to search it and register it as a normal source. ~30 lines unlocks "what did I conclude about copper last week."

Longer term: turn on the agent framework's native cross-conversation memory (a config change — the storage and embeddings already match). Run the quick fix now; converge on the native one later; never run both as permanent.

OKF shards as reasoning substrate

A raw tweet retrieved as a paragraph forces the model to re-extract "is this a signal or a risk?" every time. An OKF shard stores that structure up front — signal, risks, action items, proposals, citations — as typed fields.

So "show me the risks on copper" filters to risk-typed shards then ranks — far sharper than hoping similarity separates risk-prose from signal-prose. Retrieval returns structure, not a blob. This is exactly where the X-post / event-watch pipeline plugs in: it's the producer of these shards; this architecture is the consumer.

Part 7 — Before & after

What changes for each source

Knowledge sourceTodayAfter
Research corpusReachable, but raw top-10, no rerankOver-fetch + reranked + cited
Conversation memoryWrite-only · never readSearchable source — recall past answers
Tracked-account posts (X/OKF)Doesn't exist yetFirst-class structured source
Sector sentimentOne narrow toolIn the fan-out, provenance-chained
Past reportsNo chat accessRegistered + queryable
Emerging-topic clustersNo chat accessRegistered + queryable
A brand-new tableSilent blindspot until noticedCI fails until registered
Part 8 — Build plan

Seven phases, additive-only, independent of the demo freeze

PhaseDeliverableRisk
P0 · VerifyConfirm the keyword index exists; confirm live row counts. Hard gate — everything depends on this.read-only
P1 · Registry railsThe source registry, descriptor shape, the no-blindspot CI test. Register existing sources. No behavior change.Class C
P2 · Memory unblockMake conversation memory readable + registered. Stops the waste, adds recall.Class C
P3 · Fusion + rerankCross-source rank-fusion + revive the rerank + provenance plumbing + over-fetch.Class C
P4 · RoutingThe fan-out tool + the catalog menu + pre-parse + coverage audit + weak-result fallback.Class C
P5 · OKF sourceThe tracked-post shard table + search function + descriptor. Consumes the event-watch producer.Class C
P6 · EvalA labeled query set; measure routing recall (the no-blindspot metric), precision, answer quality.Class S
What to keep vs add. KEEP the existing agentic loop, gpt-5.2 as the router, and the single-answer utility tools. ADD only the menu, the fan-out tool, the pre-parse, fusion+rerank, and the safety net. This is a strengthening of what runs, not a rebuild.
Part 9 — Honest risks

What could still go wrong

The unverified index

If the keyword index is missing, keyword search silently scans the whole table — fine now, a latency cliff as the corpus grows. Verify in P0 before anything else.

Quality is unmeasured

"No blindspots" stays an assertion until the labeled eval set exists. The coverage-audit log makes it measurable in production.

Tuning is guesswork

Fusion weights and fallback thresholds are starting defaults, not optimized. Tune them with the feedback table that already exists.

Source descriptions are the ballgame

A mis-described source misroutes silently. No test catches semantic quality of the descriptions — only per-source eval queries do.

Rerank is an external hop

Adds latency, cost, and a single point of failure. Mitigated by reranking only the top 20 and falling back gracefully.

Code vs table for the registry

Recommendation: registry lives in code (gives the CI gate), with an optional read-only mirror to a Supabase table for dashboards. Needs operator sign-off since it touches "everything in Supabase."