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 on the pool path — since v0.7.0 the default whenever a semantic embedder is available (every keyed or production deployment): how a read decides it doesn't have the answer, what it does about it, and what happens after a failure. (The legacy unit path's version of this story is compressed at the bottom, version-scoped.)
The architecture is a stack, not a single check:
- The serve gate filters — calibrated against the pool's own measured noise.
- Attribution makes the leaks refusable — a near-miss serve produces an honest refusal, not an interpolation.
- Freshness is structural — a stale claim is masked the instant its source changes; it cannot serve.
- The RAG floor bounds the downside — an under-covered read degrades to honest retrieval, never below it.
- New in v0.7 The failure chain converts failures into permanent repairs — the one layer no filter can be: recovery.
The serve gate — calibrated against the pool's own noise
Every read ranks the global fresh-claim pool and the serve gate (stage P4) decides serve-vs-build. Its semantics are worth knowing exactly:
- The decision. Top pre-rerank claim cosine
≥the effective gate → serve; below → the build read (probe classification below). Thererankerorders serving only — 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 — against provably-unanswerable queries. The gate calibrates on 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 margin, clamped to[coverage-floor default, coverage-floor default + 0.27](~0.28 to ~0.55 on the shipped OpenAI embedder) — 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 it), at most once per 16 builds. Serve-only traffic never pays for calibration.
- An explicit float is absolute.
serve_gate=0.4disables adaptation — the off-ramp for exact, reproducible benchmarks. - The reuse bypass can't serve stale. A query whose seed cosine to a cached unit is
≥ 0.9is the same question asked again and serves regardless of the gate — but a stale reuse match re-materializes in place first. - Read it live.
stats()["serve_gate_effective"]andstats()["pool_noise_ceiling"]; a climbingbuilds_by_gapwith flat accuracy means the gate sits too high.
When the gate says build: the probe classification
A read below the gate doesn't blindly rebuild anything. It runs one probe retrieval and classifies the result per artifact:
| Class | What the probe found | Action | Synthesis cost |
|---|---|---|---|
| CONTAINED | every chunk of this source is already retained verbatim by a fresh unit | serve — provenance proves coverage | 0 |
| STALE-OWNED | a unit owns this source but is dirty/expired | rebuild that unit in place | 1 |
| THIN | a unit owns this source but didn't retain these chunks | rebuild that unit in place (widened, with widen_on_admission) | 1 |
| NOVEL | no unit owns this source | build a source-anchored unit | 1 |
Two containments bound the cost: synthesis is capped at ≤ 3 ops per read, and an all-CONTAINED probe is a structural admission — the read serves with zero synthesis, because building would mint a duplicate understanding (stats()["admission_reuses"] counts these). Empty cache vs warm cache is the same algorithm with different statistics: on an empty cache every artifact classifies NOVEL (that's warm-up, not a mode); on a warm cache most gate-misses come back CONTAINED or STALE-OWNED.
Attribution makes the leaks refusable
Cosine gates are statistical filters, not proofs — a near-miss topical serve will occasionally clear the gate. It is safe by construction because 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.
Freshness is structural, not hoped-for
On the pool path, freshness is a live pull-mask: the moment source_changed fires, every claim derived from that source is masked out of the pool — it cannot be ranked, cannot be packed, cannot serve. TTL expiry clock-compares the candidate-head owners on every read (bounded: ≤ 3 revalidator calls). A stale answer isn't filtered out late; it's structurally unable to enter the payload. See Provenance & freshness.
Rebuild vs escalation — two different failures
| Escalation (the RAG floor) | Rebuild | |
|---|---|---|
| Answers the failure | coverage — the cache is fresh but doesn't hold this fact | freshness — the cache holds it, but the source changed |
| Trigger | final coverage < coverage_floor (~0.28 OpenAI) | the freshness mask / TTL, or a STALE-OWNED / THIN probe class |
| What happens | append attributed raw retrieval ([source: id]-prefixed chunks) to the payload | re-synthesize the unit from fresh source text before serving |
| Mutates the cache? | no — a serving action only | yes — the unit re-materializes |
| LLM call | none | one synthesis call |
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? The same-extractor-same-loss finding: if the extractor dropped a fact once, re-running the same extractor over the same unchanged source mostly reproduces the same loss. So coverage gaps escalate to raw for the read at hand — and persistent extraction loss is the failure chain's job, below.
After the filters: the failure chain (v0.7)
New in v0.7 Filters can only refuse; they can't recover. The failure chain is the recovery layer — and its safety is structural, not statistical:
- The gap detector fires without a refusal. With
gap_detector=True, a read reports, per probe, when a raw evidence sentence outscores every served claim — the one signal that catches the confident-wrong class, where refusal-gated machinery is blind. Observe-only: serving is byte-identical ON vs OFF (pinned), and each gap is typedextraction_hole(repair terrain) orcorpus_hole(route to your tools — the cache never fetches). - Each rung fires only on failure and cannot un-answer a correct read.
repair(read_id)is gated on a failed first pass — your evaluator's call, never the library's.serve_unservedandreproberun on refusals — and a refusal is never a correct answer, so the downstream rungs have nothing to break. Reprobe additionally re-ranks under a MAX-union that includes the original probes, so nothing already served can rank worse. - A repair is permanent and auditable. Admitted claims append with per-claim provenance (
_repair_provenance) — traceable and evictable by inspection; admission hygiene mechanically rejects the measured defect shapes (pronoun-subject, truncated) before they can serve.
The wrong answer's last line of defense, in other words, is that a failure improves the store: the same miss never costs you twice. Full reference: The failure chain.
What's measured
The honesty properties above aren't design intent — they're benchmark rows:
| Property | Measured | Where |
|---|---|---|
| Refusal honesty on unanswerable questions | 95% vs naive top-k's 85–88% | 605-question news rig |
| Refusal repair (v0.6 behavioral stack, opt-in) | refusals 91 → 61 (−33%), zero newly-wrong answers | same rig |
| Final refusals, full v0.7 chain vs the strongest v0.6 configuration | −69% (61 → 19) | same rig + locked adjudication |
| Right-unit routing (unit path) | route@1 ≈ 1.00 | clean structured bench |
| Misattribution — a number served from the wrong source | ≈ 0–2% — the naive answerer's own noise level | same bench; see the transparency note below |
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 v0.6 measured result was parity at fewer tokens; the v0.7 gain is measured against our own strongest v0.6 configuration). The claim is narrower and stronger: wrong-source, stale, and fabricated-from-adjacent-context answers are engineered out, measured, and honestly bounded — and what still fails, heals.
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 — it fires when the unit path's single matched unit under-covers, sweeping claims from other fresh units (surfaced as result.recalled). The pool path doesn't run recall as a step — the pool scan is already cross-unit by construction: every read ranks the global fresh-claim pool from all units. The recall_* knobs are inert on the pool path and result.recalled stays [] there.
The unit path (legacy escape hatch)
Version-scoped: this section describes read_path="unit" — the pre-0.7 default, kept byte-identical as the explicit escape hatch and as the automatic keyless fallback (pool ranking needs a semantic embedder). Skip it unless you run that path.
The unit path's honesty story is two questions at two granularities. The hit gate asks "is this the right territory?" — a blended 0.7·topic + 0.3·seed cosine over the whole unit, floored at ~0.33 (OpenAI). Topic similarity is diffuse: a leave-policy unit scores respectably against any leave-adjacent question. Coverage then asks "does any single claim actually answer this?" — the max single-claim cosine, floored at ~0.28. Fact similarity is sharp: a sick-days query matches a vacation-policy unit's territory but no individual claim, so the read escalates instead of answering thin. (This is why understanding moved from prose to atomic claims in v0.4 — a gate can only be as sharp as the thing it scores; claim granularity is what makes "relevant but insufficient" detectable.) The same RAG floor, S2 band (coverage_scorer), and freshness gates apply; widen_chunks New in v0.5 fixes the build-time keyhole (widened units read a median 23 chunks of their source vs 2 — rebuild churn fell 460 → 31); and route@1 ≈ 1.00 / misattribution ≈ 0–2% in the table above were measured on this path.
Next
- The gate ladder — both firing orders, every knob, gate by gate.
- The failure chain — the recovery layer in full: signals, verbs, pinned guarantees.
- Context intelligence — the pool payload contract and the behavioral stack in detail.
- Benchmark — every number above, with methodology.