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.

searchchat-ui

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.

So what Worth importing if your fork ships RAG citations over SSE. The chunk_id propagation pattern replaces fragile text-anchor-based linkage with stable ids that survive the model's text generation. The string-ref tolerance fix is defensive hygiene that any Claude RAG consumer should apply - the model produces both types in the wild.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

4 commits from Davemaina1/iroh_, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
6e12f686 Citations: open web-source URLs in new tab instead of DocPanel Davemaina1 2026-05-13 ↗ GitHub
68b457c1 Citations: stable chunk_ids end-to-end via citation_sources SSE event Davemaina1 2026-05-13 ↗ GitHub
b75b6611 fix(citations): tolerate string ref in LLM output, log SSE emission Davemaina1 2026-05-14 ↗ GitHub
2b142754 fix(backend): resolve TS error in chatTools citation log line Davemaina1 2026-05-14 ↗ GitHub
Cast citations[0] to Record<string, unknown> to satisfy tsc strict
property access check on the conditionally-typed array.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-381.md from inside the repo you want the changes in.

⬇ Download capture-thread-381.md