What knowledge context does — and why it matters in a meeting
You're in a meeting. Someone says "Should we use the new Glean embedding model?" — and instantly, a card slides in on the right side of your screen showing the relevant internal docs about that exact topic.
Nobody searched for anything. Nobody opened a new tab. The app listened to the conversation and surfaced what everyone needed, right when they needed it.
This branch adds ~5,100 lines of new code that make Eridanus understand what people are talking about in real time and proactively surface relevant company knowledge.
Here's the quick version of what the system does:
Every finalized transcript segment gets fed to the knowledge pipeline
An LLM extracts the current topic, open questions, and tensions from a rolling window of conversation
When the topic changes significantly, it queries Glean's knowledge base with auto-generated search queries
A surfacing gate scores results on relevance, actionability, and novelty before showing them
Relevant context is broadcast to every participant via WebSocket and appears as collapsible cards
This is a real-world example of a pipeline architecture. The same pattern — listen, understand, act — powers recommendation engines, content moderation, and AI assistants everywhere.
The six components that make knowledge context work
Think of this like a newsroom. Each person has a specialty, and they pass information along a chain until a story is ready to publish.
The editor-in-chief. Receives transcript segments, decides when to trigger a search, and coordinates everything.
The beat reporter. Keeps a rolling window of conversation and uses an LLM to extract the current topic, questions, and tensions.
The research desk. Queries the company's knowledge base (Glean) via either search or chat API.
The quality editor. Uses an LLM to score whether results are actually worth interrupting the meeting for.
The fact-checker. Weeds out filler-heavy utterances and detects conversation triggers like questions and disagreements.
The front page. Renders collapsible knowledge cards with markdown answers and clickable citations.
The Source interface is the linchpin of this design. It defines three methods — Search, Name, Available — that ANY knowledge backend must implement. Today it's Glean. Tomorrow it could be Notion, Confluence, or a local file search. The rest of the pipeline doesn't care which one it's talking to. This "code against an interface, not an implementation" pattern is one of the most powerful ideas in software engineering.
Tracing the path from "Should we use Vitest?" to a knowledge card on every participant's screen
Imagine someone in your meeting says "Should we switch from Jest to Vitest?" — here's the exact path that sentence takes through the system.
When the Hub receives a final (non-partial) transcript segment, it does two things simultaneously: broadcasts the text to other participants for their transcript view, and forwards it to the knowledge pipeline.
// Feed final segments to the knowledge pipeline
if c.Hub.knowledgePipeline != nil {
c.Hub.knowledgePipeline.OnSegment(c.SessionID, KnowledgeSegment{
Speaker: seg.SpeakerName,
Text: seg.OriginalText,
Language: seg.OriginalLanguage,
IsFinal: true,
})
}
If the knowledge system is turned on...
Tell it about this new sentence someone just said, including who said it, what they said, and what language it was in.
Mark it as "final" (not a mid-sentence preview) so the pipeline knows it's safe to analyze.
The Pipeline is the conductor. It first checks if the utterance is worth analyzing, then updates the conversation state, and only triggers a search when the topic has genuinely changed.
func (p *Pipeline) OnSegment(sessionID string, seg Segment) {
// Pre-filter: skip short or filler-heavy utterances
if !isSubstantive(seg.Text) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), llmTimeout)
defer cancel()
if err := p.tracker.Update(ctx, sessionID, seg); err != nil {
return
}
if !p.tracker.HasChangedSignificantly(sessionID) {
return
}
p.tracker.AckBroadcast(sessionID)
p.scheduleSearch(sessionID)
}
When a new sentence arrives...
First, is this sentence worth analyzing? If it's just "yeah, um, ok" — skip it entirely.
Set a 10-second time limit for the LLM call, then ask the StateTracker to update its understanding of the conversation.
Has the topic actually changed from what we've already shown? If not, don't search again — we don't want to spam people with the same results.
Mark this topic as "handled" immediately (so concurrent segments don't trigger duplicate searches), then schedule a knowledge search.
How conversation state tracking turns messy speech into structured understanding
Think of the StateTracker like a court stenographer with a photographic memory — but they only remember the last 20 sentences. As new sentences come in, old ones scroll off. This "rolling window" is the raw material the LLM uses to understand what's happening.
From that messy conversation, the LLM produces a structured JSON snapshot called the ConversationState:
"Vitest vs Jest migration" — the specific subject, not a vague label like "testing discussion"
"Can Vitest run Jest tests with minimal changes?" — things that are still unresolved
"Speed vs migration risk" — disagreements between participants
"vitest jest migration compatibility" — auto-generated search terms for the knowledge base
How does the system know when the conversation has moved to a genuinely new topic? It uses Jaccard similarity on content words — filtering out common words like "the", "is", "what" — to compare the new topic against ALL previously shown topics.
func wordSimilarity(a, b string) float64 {
setA := contentWordSet(strings.ToLower(a))
setB := contentWordSet(strings.ToLower(b))
if len(setA) == 0 && len(setB) == 0 {
return 1
}
intersection := 0
for w := range setA {
if setB[w] {
intersection++
}
}
union := len(setA)
for w := range setB {
if !setA[w] { union++ }
}
return float64(intersection) / float64(union)
}
Take two topic strings and score how similar they are (0 to 1).
First, extract only the "meaningful" words from each — remove filler words like "the", "is", "and".
If both topics have no meaningful words, treat them as identical.
Count how many meaningful words appear in BOTH topics (the overlap).
Count the total number of unique meaningful words across both topics.
The similarity is: overlap ÷ total. If it's above 0.5 (50%), the topics are "too similar" to trigger a new search.
Words like "the", "is", "what" appear in almost every sentence. Including them in similarity comparisons would make everything look similar. By filtering them out, the system compares only the meaningful content words — the signal, not the noise. This same technique is used in search engines, document classification, and spam filters. When working with AI tools, you'll see "stop word filtering" pop up everywhere text analysis happens.
How the system avoids spamming your meeting with irrelevant cards
Surfacing information in a live meeting is high-stakes. Show something irrelevant, and people start ignoring the feature. Show too often, and it becomes noise. The system has four layers of protection, like a building with a bouncer, a metal detector, an ID check, and a guest list.
Rejects short sentences (<8 words) and filler-heavy speech (>60% words like "yeah", "um", "ok")
Minimum 60 seconds between broadcasts. Even if topics shift rapidly, results queue up instead of flooding.
Compares new topics against ALL previously shown topics using word similarity. Won't re-surface similar content.
An LLM scores results on relevance, actionability, and novelty. Below 0.6? Rejected. Also penalizes recently shown titles.
func isSubstantive(text string) bool {
trimmed := strings.TrimSpace(text)
if len(trimmed) < minCharCount {
return false
}
words := strings.Fields(strings.ToLower(trimmed))
if len(words) < minWordCount {
return false
}
fillerCount := 0
for _, w := range words {
clean := strings.Trim(w, ".,!?;:'\"")
if fillerWords[clean] {
fillerCount++
}
}
return float64(fillerCount)/float64(len(words)) <= maxFillerRatio
}
Is this sentence worth analyzing? Let's check three things.
First, is it long enough? At least 30 characters. "Yeah" doesn't make the cut.
Second, does it have enough words? At least 8. "Sounds good, right?" doesn't either.
Third, strip punctuation from each word and check if it's a filler word (yeah, um, ok, like, basically...).
If more than 60% of the words are filler, this sentence is just noise. Skip it.
Beyond topic changes, the system also detects specific signals in speech that suggest a knowledge search would be useful. Think of it like a journalist's instinct for a story — certain phrases are red flags that mean "someone needs information."
question"?" or starts with "what", "how", "why", "should", "do you think"decision"should we", "let's go with", "which one", "the plan is"disagreement"i disagree", "that won't work", "on the other hand"assumption"i assume", "what if", "suppose we"Debouncing is crucial here. In a fast conversation, the topic might "change" five times in 30 seconds as different speakers riff on related ideas. Instead of firing five searches, the pipeline resets a timer on each change and only executes when the conversation settles. This is the same technique used in search-as-you-type features.
Apply what you've learned to new scenarios
You want to add Notion as a second knowledge source alongside Glean. Which file do you need to create, and which existing file do you need to modify?
Users report that knowledge cards keep appearing for the same topic even though the conversation hasn't moved on. The topic is "Q3 budget planning" and it keeps re-appearing every minute.
You're building a similar knowledge feature for a different app. Your users complain that the system surfaces too many irrelevant results — lots of quantity, low quality. You have both search and chat API access.
Two people in a meeting speak at nearly the same time. Both their finalized segments arrive at the Pipeline within milliseconds of each other. Without any protection, what would happen?
AckBroadcast solve?