Skip to documentation

Lens

Conversation threading & grounded chat

Chat in eli.ai is a durable, multi-turn conversation, not a stateless prompt. Every turn is persisted server-side; generated answer claims carry clickable [Sn] citations that survive the thread, and an asynchronous groundedness check attaches a "verified n/m" badge to each answer. This page covers how threads are stored, how citations stay stable across turns, and how the badge is computed.

A turn, end to end

One conversation turn: load history → retrieve → generate → persist → verify.

Server-side persistence

A conversation is a first-class, workspace-scoped record. The first turn creates a conversation and returns its id; subsequent turns pass that conversationId back so the server can rehydrate the full thread. History lives in Postgres under row-level security — a conversation in another workspace is simply invisible — so a thread is durable across reloads, devices, and worker-driven agent turns alike. Nothing about the conversation lives only in the browser.

  • Messages store the role, the rendered text, and — for assistant turns — the source map that resolves each [Sn] label to a chunkId/docId.
  • Titles are derived from the opening turn so the conversation list is scannable.
  • Token & cost accountingattaches to each turn, so a long thread's spend shows up on the cost dashboard like any other model usage.

The same threading powers agents

Agent runs accept the same conversationId, so an API-driven multi-turn agent and the chat UI share one persistence model. See Agents & runs (v1).

Source Citations across turns

Retrieval runs per turn and labels its sources [S1], [S2], … The labels are local to the turn — turn 3's [S1]may be a different chunk than turn 1's — and each turn's source map is stored alongside its message. When you reopen a thread days later, every citation in every past answer still resolves and still jumps to the exact source chunk, because the mapping was persisted, not recomputed.

Sources stream to the client before the answer tokens, so citations are clickable the instant they render; a small hold-back buffer prevents a half-written [S1 from flickering. The underlying retrieval, fusion, and refusal behavior is identical to a single-shot answer — see Retrieval & grounded chat.

Citations resolve to the same source on every reopentext
Turn 1  → "…froze Operations headcount [S2]."   [S2] = doc 01J… · chunk 01J…
Turn 3  → "That freeze was tied to the Q1 miss [S1]."  [S1] = doc 01J… · chunk 01J…
                                     ▲ per-turn label, persisted, always clickable

The groundedness badge

After an answer finishes streaming, the server enqueues an asynchronous groundedness check that runs the shared judgeAssertionsengine — the same checker the evals runner uses — over the answer's claims and its cited evidence. The result attaches to the message as a "verified n/m claims" badge: n claims were supported by the sources out of m checked.

  • Asynchronous. The badge resolves after the answer is already on screen, so verification never adds latency to the response.
  • Flag-only. In interactive chat the check never hard-blocks or silently rewrites an answer. A noisy judge must not fail in front of a client; the strict, claim-by-claim NLI variant runs only in eval runs.
  • Persisted.The verified count is stored on the turn, so the badge is there when you reopen the thread — no recomputation, no drift between what you saw and what's recorded.

Reading the badge

A full m/mmeans every checked claim was grounded in the cited sources. A shortfall is a prompt to click through the citations — it flags claims the evidence didn't clearly support, not a verdict that the answer is wrong.

Related