Stable chunk_ids travel from retrieval tool to UI via a citation_sources SSE event
Four commits rebuild how RAG citations flow from tool result to the UI in Davemaina1's fork. The core change - assigning stable ids at retrieval time and propagating them as a side-channel SSE event - is applicable to any RAG product that needs to link citations back to their sources without re-parsing text.
The first fix (6e12f686) is small but catches a real bug: if a citation's doc_id is a URL (which happens when the model cites a web source like kenyalaw.org), openCitation in ChatView.tsx routes it to DocPanel, which issues GET /single-documents/{id}/docx and 404s. The fix is a URL prefix check - docIdStr.startsWith("http://") or "https://" - that opens a new tab instead.
The main work is 68b457c1. At tool-call time, each result from search_kenya_law and search_legal_authority_web gets a chunk_id: corpus:{tc.id}:{i} for corpus results, web:{tc.id}:{i} for web results, where tc.id is the tool call id and i is the result index. These ids appear in the JSON passed back to the model and are also collected in citationSourcesCollector. After each tool-call round, if the collector is non-empty, a citation_sources SSE event fires carrying { chunk_id, url, title, source_label } for each result. The system prompt gains a schema block telling the model to copy chunk_id verbatim into citation doc_id fields; the same instruction repeats inline in each tool response as _citation_instructions.
At stream end, runLLMStream builds a Map<string, string> from all citation_sources events seen during the turn, keyed by chunk_id. When resolving a citation's display filename, the fallback chain is now docInfo?.filename ?? chunkTitleMap.get(c.doc_id) ?? c.doc_id. The same map is built in extractAnnotations for the non-streaming path.
b75b6611 handles real-world model behavior: Claude sometimes outputs citation ref as a string ("1") instead of a number. The normalizeCitation function previously rejected these; now it coerces string refs with parseInt(c.ref, 10) and checks Number.isFinite. 2b14275 adds a TypeScript cast on a debug log line to satisfy strict mode.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?