Core concepts

Why Coalent doesn't serve the wrong answer

How a RAG cache knows it doesn't have the answer — two-granularity gating, honest attributed refusals, rebuild vs escalation, and the adaptive serve gate.

The RAG failure that hurts isn't a missing answer — it's a confident wrong one: an answer hallucinated over stale or merely-adjacent context. This page walks Coalent's honesty architecture end to end: how a read decides it doesn't have the answer, what it does about it, and what's measured.

One rule for reading it: the unit path (read_path="unit", the default) and the pool path (read_path="pool", opt-in v0.6) are separate machines. Every section below says which one it's describing. The gate ladder has both firing orders in full.

Matching is not sufficiency

The unit path asks two different questions at two different granularities, and the distinction is the whole story:

  • The hit gate (gate 1) asks: "is this the right territory?" It scores the whole unit — a blended 0.7·topic + 0.3·seed cosine against the query, floored at ~0.33 (shipped OpenAI embedder). Topic similarity is diffuse: a unit about leave policy scores respectably against any leave-adjacent question.
  • Coverage (gate 4) asks: "does any single claim actually answer this?" It is the max single-claim cosine — the best individual claim in the matched unit against the query, floored at ~0.28. Fact similarity is sharp: a claim either speaks to the question or it doesn't, and the score falls off fast.

Worked example — a cached unit built from hr-policy.md holding the claim "Employees accrue 21 days of annual vacation leave per year.":

QueryHit gate (unit topic)Coverage (best single claim)Outcome
"how much annual leave do I get?"high — right territoryhigh — the vacation claim answers it directlyserve from cache
"how many sick days do I get?"mid — still leave territory, clears ~0.33low — no claim mentions sick leave; the best cosine sits near the ~0.28 floorescalate — the unit matched, but nothing in it answers

Rough bands, illustrative not contractual: topic cosines for both queries land in the same comfortable-pass region, while the claim cosines split cleanly — that separation is what claim-level granularity buys. A whole-summary similarity check would score the sick-leave query "covered" on vibes; the max-single-claim check can't, because no individual claim is about sick leave.

NOTE

This is why v0.4 moved understanding from prose summaries to atomic extractive claims — not just for token efficiency. A gate can only be as sharp as the thing it scores. Claim granularity is what makes "relevant but insufficient" detectable.

The gate is a statistical filter — the stack is the guarantee

Honest framing: cosine gates are statistical filters, not proofs. The score distributions of "covered" and "topically adjacent" overlap at the tails, so a near-miss topical serve will occasionally clear both gates. Coalent's position is that this is safe by construction, because no single check is the guarantee — the stack is:

  1. The gates filter most of it. Two granularities (territory, then fact) catch the bulk of insufficient hits and route them to escalation or a build.
  2. Attribution makes the leaks refusable. The payload is never an anonymous prose blob — it's attributed atomic claims under source headers. When a near-miss serves, the answer model sees claims about vacation accrual labeled as such, finds nothing about sick days, and refuses honestly instead of interpolating. Measured on the 605-question news rig: 95% refusal honesty on unanswerable questions, vs 85–88% for naive top-k — a stuffed raw-chunk context invites the model to synthesize something from adjacent noise; a short list of attributed claims makes "the context doesn't say" the easy completion.
  3. The refusal loop repairs. On the pool path with the behavioral stack armed (residual_spans=True, default OFF), a downstream refusal re-enters the cache: report_refusal(read_id) returns an attributed retry payload from captured source spans, report_success(read_id) confirms the rescue, and repeated signals mark the owning unit for append-only repair on its next rebuild touch. Measured: refusals 91 → 61 (−33%) with zero newly-wrong answers. See the behavioral stack.

A filter that catches most, a payload that makes the rest refusable, and a loop that converts refusals into repairs — that composition, not any one threshold, is why the wrong answer doesn't ship.

When the gate says build: the probe classification (pool path)

On the pool path, a read whose best pre-rerank cosine falls below the serve gate doesn't blindly rebuild anything. It runs one probe retrieval (the read's single query-shaped retrieval) and classifies the result per artifact:

ClassWhat the probe foundActionSynthesis cost
CONTAINEDevery chunk of this source is already retained verbatim by a fresh unitserve — provenance proves coverage0
STALE-OWNEDa unit owns this source but is dirty/expiredrebuild that unit in place1
THINa unit owns this source but didn't retain these chunks — it under-covers its own sourcerebuild that unit in place (widened, with widen_on_admission)1
NOVELno unit owns this sourcebuild a source-anchored unit (one per namespace + artifact, always)1

Two containments bound the cost. Synthesis is capped at ≤ 3 ops per read — groups are ranked best max-chunk-cosine first, and the rest converge on later reads. And an all-CONTAINED probe is a structural admission: the read serves with zero synthesis, because building here would mint a duplicate understanding of sources the cache already understands (stats()["admission_reuses"] counts these).

Empty cache vs warm cache is the same algorithm with different statistics. On an empty cache every probe artifact classifies NOVEL, so every read builds — that's warm-up, not a special cold-start mode. On a warm cache most gate-misses come back CONTAINED or STALE-OWNED — a free serve or one surgical rebuild. Nothing switches over; only the classification distribution moves as the cache learns the corpus.

Rebuild vs escalation

These two are constantly conflated, and they answer different failures:

Escalation (the RAG floor)Rebuild
Answers the failurecoverage — the cache is fresh but doesn't hold this factfreshness — the cache holds it, but the source changed
Triggerfinal coverage < coverage_floor (~0.28 OpenAI)a dirty / TTL-expired unit (unit path gate 3; pool P3 + the freshness mask), or a STALE-OWNED / THIN probe class
What happensappend attributed raw retrieval ([source: id]-prefixed chunks) to the payloadre-synthesize the unit from fresh source text before serving
Mutates the cache?no — a serving action onlyyes — the unit re-materializes
LLM callnoneone synthesis call
Which pathboth — coverage_floor keeps its job on unit and poolboth
TIP

The RAG-floor covenant. An escalated read never carries less context than plain RAG would have retrieved: the floor appends attributed raw chunks — deduplicated, budget-capped, never empty. Under-coverage degrades Coalent to honest retrieval, never below it.

Why doesn't a coverage gap just trigger a rebuild? Because of the same-extractor-same-loss finding: if the extractor dropped a fact from a source once, re-running the same extractor over the same unchanged source mostly reproduces the same loss — a synthesis call spent keeping the same gap. So coverage gaps probe first (classify, serve or surgically rebuild what the probe actually implicates) and escalate to raw for the read at hand. Persistent extraction loss is the behavioral repair loop's job: span serves and raw fallbacks accumulate against the owning unit, lossy_threshold (default 2) marks it lossy, and its next rebuild touch repairs append-only — claims are never dropped while the source hash is unchanged.

Widening: fix the keyhole at build time

A miss-triggered build sees only the retrieval keyhole — the top-k chunks for the one query that caused the miss. A unit built through a keyhole under-covers every later question about the same source, which shows up as escalation and rebuild churn. widen_chunks=N New in v0.5 lets the build read up to N chunks of the dominant source instead (a duck-typed retriever.widen(artifact_id, limit=) or a BYO source_fetcher). In the cold-start pilot, widened units read a median 23 chunks of their source vs 2 for keyhole builds — and rebuild churn fell 460 → 31. It never fires at ingest: units stay lazy, built only when a query needs one.

The serve gate, concretely (pool path)

The serve gate is the pool path's serve-vs-build decision (stage P4), and its semantics are worth knowing exactly:

  • The decision. Top pre-rerank claim cosine the effective gate → serve; below → the build read (probe classification above). The reranker orders serving — it can never cause a false serve or a skipped build, because the decision always reads the pre-rerank score.
  • serve_gate=None (default) is adaptive. The gate calibrates against the pool's own null-shaped noise ceiling — the p95 of max claim cosines measured on provenance-disjoint probes (≤ 24 fixed-seed units): how high does a claim score on a query it provably cannot answer? The effective gate is that ceiling plus a small margin, clamped to the band [coverage-floor default, coverage-floor default + 0.27] (~0.28 to ~0.55 on the shipped OpenAI embedder) — so a dense near-duplicate pool can never inflate the gate high enough to refuse real answers.
  • Recalibration is bounded and hidden. The ceiling recomputes at the end of a build read (post-synthesis, where LLM latency hides the cost; the in-flight read used the previous ceiling), at most once per 16 builds. Serve-only traffic never pays for calibration.
  • An explicit float is absolute. serve_gate=0.4 disables adaptation entirely — the operator's off-ramp for exact, reproducible benchmarks.
  • Read it live. stats()["serve_gate_effective"] is the gate actually applied this instant, alongside stats()["pool_noise_ceiling"]. A climbing builds_by_gap with flat accuracy means the gate sits too high.
  • The reuse bypass. A query whose seed cosine to a cached unit is ≥ 0.9 (reuse_threshold) is the same question asked again and serves regardless of the gate — and a stale reuse match re-materializes in place first, so the bypass can never serve stale.

What's measured

The honesty properties above aren't design intent — they're benchmark rows:

PropertyMeasuredWhere
Right-unit routingroute@1 ≈ 1.00clean structured bench (unit path)
Misattribution — a number served from the wrong source≈ 0–2% — the naive answerer's own noise levelsame bench; see the forensics story below
Refusal honesty on unanswerable questions95% vs naive top-k's 85–88%605-question news rig
Refusal repair (behavioral stack, opt-in)refusals 91 → 61 (−33%), zero newly-wrong answerssame rig

The misattribution number has a story worth reading: an earlier internal run showed a large misattribution rate that turned out to be a benchmark bug — contradictory duplicate sources no router could resolve — and the transparency note on the benchmark page documents the forensics in full. And the standing anti-claim applies here too: none of this claims accuracy above naive RAG (the measured result is parity at fewer tokens). The claim is narrower and stronger: wrong-source, stale, and fabricated-from-adjacent-context answers are engineered out, measured, and honestly bounded.

One clarification: the pool path has no recall step

A recurring reading error, worth killing explicitly: cross-unit recall is a unit-path mechanism New in v0.4 — gate 5, firing when the single matched unit under-covers, sweeping the best claims from other fresh units to bridge the gap (surfaced as result.recalled).

The pool path doesn't run recall as a step — the pool scan is already cross-unit by construction. Stage P2 ranks the global fresh-claim pool from all units on every read; there is no anchored unit to recall around. Accordingly the recall_* knobs are inert on the pool path and result.recalled stays [] there. If you see prose implying the pool path "runs cross-unit recall", it's describing the unit path.

Next