Every get(query) walks a fixed ladder of gates, top to bottom. There are two ladders, selected by one constructor knob — and New in v0.7 the pool path (read_path="pool" — pooled cross-unit claim ranking, budgeted packing, an adaptive serve gate) is the default whenever a semantic embedder is available, which is every keyed or production deployment. It is the path every published benchmark number was measured on, and the path all v0.7 features (the failure chain included) run on. The unit path remains as the legacy escape hatch and the keyless fallback — pass read_path="unit" for byte-identical pre-0.7 behavior. Each rung is also a knob — but you rarely touch one. The defaults are pure cosine: both ladders run with no extra model and no extra dependency, and only reach for raw when the cache genuinely under-covers.
v0.7.0 makes one deliberate default change. Since 0.7.0 the read path defaults to pool whenever a semantic embedder is available — which is every keyed or production deployment. Keyless zero-config construction falls back to the unit path with a warning (pool ranking is measured-unsafe on the keyword-hash fallback embedder). Pass read_path="unit" or "pool" to pin either explicitly; explicit "unit" is byte-identical to pre-0.7 behavior. The measured path is now the default path — 0.6/0.6.1 were the transition releases where it shipped opt-in — and every new v0.7 knob (gap detector, repair, subs/constraints, ...) remains default-OFF and byte-inert until armed.
get() dispatches on read_path first: "pool" (the default under a semantic embedder) runs the pool sequence; "unit" (legacy / keyless fallback) walks the eight-gate ladder exactly as in v0.5. On the pool path the unit-path read knobs (hit_threshold, hit_margin, route_by_claim, recall_*, select_floor, ...) are inert — but coverage_floor (the RAG floor) and the S2 band (coverage_scorer) keep their jobs on both paths. Pool ranking needs a semantic embedder: keyless auto-construction resolves to the unit path with a warning, and explicitly pinning read_path="pool" under the lexical HashingEmbedder still raises (a keyword cosine cannot rank claims) — set OPENAI_API_KEY or pass embedder=.
The two ladders, in firing order
A read flows down its spine — pooled or matched, fresh, covered, served. Each gate can branch off: a below-gate read (pool) or a miss (unit) builds, a stale unit re-materializes, an under-covered serve escalates to attributed raw (the RAG floor), and — with the behavioral stack armed — a downstream refusal re-enters through report_refusal. It never fabricates.
Pool path ladder (the default with a semantic embedder)
On the pool path — the default since v0.7.0 whenever a semantic embedder is available, shipped opt-in through the v0.6 transition releases — every read is answered by budget-packing the global fresh-claim pool — the best claims from any fresh unit, ranked against the query — while units remain the ownership / freshness / build / provenance skeleton. Validated at n=605 (strict grading): 0.731 accuracy @ 981 mean tokens with a metadata pool_header — naive top-9's accuracy (0.711 @ 1311) at ~25% fewer context tokens, and naive's best measured point (k12: 0.731 @ 1729) at ~43% fewer; gold-claim serving rank p50/p75/p90 = 1/6/15. The v0.5 preview point (0.699 @ 1036) holds. This is parity at fewer tokens, not an accuracy win over naive — and the shipping default header grades 0.68 on the same rig; the 0.73 rung needs your metadata (a pool_header callable, or New in v0.7 simply meta= at ingest — the default header is now metadata-first).
A pool read runs these stages in order — the code's own stage names:
| Stage | What runs | Branch |
|---|---|---|
| P0 — validate / embed | Embed the query once; mint a deterministic read_id (the handle for report_refusal / report_success) | — |
| P1 — reuse | Seed cosine ≥ reuse_threshold (0.9) = the same question asked again → forced serve, regardless of the gate | a stale reuse match re-materializes in place first; a lossy-marked one (behavioral stack) repairs append-only first — the serve is never stale |
| P2 — pool scan | The ClaimIndex cosine scan over the namespace's claim rows; freshness is a live pull-mask — a stale unit's claims never serve | — |
| P3 — bounded TTL + key overlay | Clock-compare the candidate-head owners only (≤ 3 revalidator calls, ≤ 1 re-scan); then query_keys overlays the scan: a claim's serving score becomes max(content, key), keys counting only at/above key_floor | TTL-expired owners drop out of the candidates |
| P4 — serve gate | Top pre-rerank cosine ≥ the effective serve_gate → serve; below → build. The reranker can never influence this decision | below the gate → P5 |
| P5 — build | One probe retrieval (the read's single query-shaped retrieval), classified per artifact: CONTAINED (zero synthesis — provenance proves coverage) · STALE-OWNED / THIN (rebuild that unit in place) · NOVEL (build a source-anchored unit); ≤ 3 synthesis ops per read, then re-scan the pool | an all-contained probe is a structural admission — serve with zero synthesis |
| serving pipeline | Dedup: within-owner-only near-dup collapse (0.95) — cross-owner duplicates survive as corroboration → rerank hook (serving order only) → pack to serve_budget: headers and separators count, stop at the first overflow, at-least-one always serves | — |
| span side channel | (opt-in residual_spans) a fresh unit's residual span that outranks every fresh claim by span_margin serves as a labeled [source excerpt] line inside the same budget (≤ 2 per read) | each span serve counts a lossy signal on its owner |
| P6 — S2 band | The opt-in coverage_scorer judges the exact texts that will serve, only inside [coverage_floor, coverage_ceiling) | — |
| P7 — RAG floor | Final coverage < coverage_floor → append attributed raw ([source: id]-prefixed chunks), deduplicated, budget-capped, never empty | raw priority: the P5 probe → a reuse-rebuild's chunks → retrieve now |
| P8 — pack & serve | Render claim groups under their attribution headers; cache_hit on this path = zero synthesis calls ran | — |
Three guard rails fire at construction, not mid-read: read_path="pool" under the lexical HashingEmbedder raises (claim cosine collapses to keyword overlap); query_keys=True off the pool path raises (keys could attach and confirm yet structurally never fire); and pool without a pool_header warns — attribution falls back to the built-in header, the middle rung of a measured ~9-point ladder (below).
The pool knobs
| Knob | Default | What it gates |
|---|---|---|
read_path | auto New in v0.7 | Which ladder runs. Auto resolves to "pool" (claim-pool-first serving) whenever a semantic embedder is available; keyless construction falls back to "unit" with a warning. Pin either explicitly — explicit "unit" is byte-identical pre-0.7 behavior |
serve_budget | None → 600 unit / 1000 pool | Packed payload size (tokens; headers counted). The one knob most users touch; <= 0 raises |
serve_gate | None (adaptive) | The P4 serve-vs-build decision. Explicit float = absolute (reproducible benches); None adapts against the pool's null-shaped noise ceiling — floored at the embedder-derived coverage default (~0.28 on the shipped OpenAI embedder), ceilinged at that + 0.27 |
pool_header | None → built-in ## {query[:60]}, else [source: {artifact_id}] | Per-source-group attribution line. A measured ladder (n=605, strict): opaque id 0.64 → shipping default 0.68 → metadata callable [title | source | date] 0.73 — wire your own metadata (see the golden path). A raising hook is swallowed, never breaks serving |
reranker | None | Serving order only — never serve/build/floor decisions, so a bad reranker can't cause a false serve or a skipped build |
claim_index | None → built-in | BYO pool storage (ClaimIndex protocol; numpy / pure-python built-ins). A bare instance is single-namespace only; pass the factory form for multi-namespace |
coverage_floor | auto (~0.28 OpenAI) | The same knob as unit gate 7 — on the pool path it is the escalation floor, read against the final (post-build, post-S2) coverage |
Unit path ladder (legacy escape hatch · keyless fallback)
read_path="unit" is the legacy escape hatch — byte-identical pre-0.7 behavior: the same eight gates, same defaults, same firing order as v0.5. It was the shipped default through v0.6; since New in v0.7 it runs when you pin it explicitly, or automatically (with a warning) when construction is keyless — the lexical fallback embedder cannot rank a claim pool. (The matched unit is a list of query-independent extractive claims — see The Synthesizer — and cross-unit recall is on on this path, gate 5, so multi-hop questions resolve for free.)
| # | Gate | Default | Fires when → what happens |
|---|---|---|---|
| 1 | hit_threshold | auto (~0.33 OpenAI) | best unit's blended score 0.7·topic + 0.3·seed is below the floor → MISS: retrieve + synthesize a new unit |
| 2 | hit_margin | 0.0 (off) | top unit beats the runner-up by less than the margin → ambiguous: build the query's own unit instead of guessing |
| 3 | freshness | always on | matched unit is dirty or expired → re-materialize it before serving |
| 4 | coverage | max per-claim cosine | always computed — how well the matched unit covers the query; gates 5–7 read this value |
| 5 | cross_unit_recall | on | coverage < recall_threshold → sweep the best claims across all fresh units (MaxSim) and surface bridge facts; lifts coverage, no LLM call. A unit-path mechanism — the pool path has no recall step (its scan is already cross-unit) |
| 6 | coverage_scorer (S2) | None (off) | coverage lands in [coverage_floor, coverage_ceiling) → a cross-encoder / NLI / one-token-LLM check overrides cosine |
| 7 | coverage_floor | auto (~0.28 OpenAI) | coverage still below the floor → ESCALATE: append fresh raw retrieval (no LLM call) — the RAG floor |
| 8 | select_floor | None (lexical trim) | serve the atoms whose per-claim cosine ≥ floor — the query-relevant claims by meaning, fewer tokens. Deprecated in v0.5 — superseded by the pool read path (read_path="pool") |
A hit that survives gate 1–2, is fresh (3), covers the query (4–7), and is trimmed (8) is served straight from the cache. A miss at gate 1 or an ambiguous tie at gate 2 builds a new unit; an under-covered hit at gate 7 escalates to fresh raw — never a fabricated answer.
Before the ladder: residual_floor (build time)
residual_floor runs once, at build, not on the read path. When the extractor drops a number-bearing source span — its max per-claim cosine falls below the floor — that span is kept verbatim as an extra atom bound to the unit, closing the extractor-recall gap for stray numbers. residual_limit (default 24) caps how many are kept, ranked most-missed first, so the safety net can't defeat the token win. Embedding-only, no extra LLM call; None = off.
v0.5 hooks around the unit ladder
New in v0.5 Five knobs sit at fixed points around the eight gates. All default off; preset="multi_hop" arms the recall pair with calibrated thresholds, and explicit kwargs always override the preset.
| Knob | Where it sits | What it does |
|---|---|---|
adaptive_hit | gate 1 | the hit bar self-calibrates against cross-unit score inflation as the cache grows; repeat/paraphrase queries keep hitting via the seed-reuse channel |
provenance_admission | before any build | an exact-text containment probe — covered reads serve without building (duplicate-understanding prevention) |
widen_chunks | on a miss (build) | the build reads up to N chunks of the dominant source (a duck-typed retriever.widen(artifact_id, limit=) or a BYO source_fetcher) instead of the retrieval keyhole; never fires at ingest. widen_on_admission extends it to thin-coverage admission rebuilds |
recall_bridge / bridge_limit | gate 5 | a hop-2 restart: rank other units' claims by similarity to the matched unit's own claims, reaching the second hop the query alone can't. Armed by preset="multi_hop" |
serve="pool" / serve_budget | after the ladder | the v0.5 preview of pool serving — deprecated in v0.6, superseded by the pool read path (now the default) and never auto-mapped to it. Its deprecation clock runs from 0.7.0, the release that took the flip. See Context intelligence |
Behavioral triggers (default-OFF)
New in v0.6 The behavioral stack sits around the pool ladder, not on it — nothing below runs unless switched on, and the pool defaults are byte-identical with it off. The loop: build-time span capture → read-time side channel → your answerer refuses → report_refusal(read_id) returns an attributed retry payload → report_success(read_id) confirms the rescue as a durable query key → the key overlays the next scan (P3) → repeated signals mark the unit lossy → append-only repair on its next rebuild touch. Measured on the n=605 rig: refusals 91 → 61 (−33%) with zero newly-wrong answers; the full stack +3.1 pts same-store at ~2% token cost; keyed-class first-pass 0% → 61% on mild paraphrases (small n — keys are a latency/round-trip lever, not an accuracy lever on diverse rewordings).
| Knob | Default | What it gates |
|---|---|---|
residual_spans | False | The master switch: build-time span capture, side-channel serving, the report_refusal fallback, lossy marking, append-only repair |
span_tau | 0.62 | Build-time capture: a fact-bearing source sentence whose max claim cosine is below this becomes a tier-2 span on the unit, never in the pool |
span_margin | 0.0 | Read-time rule: a span must outrank the best fresh claim by this margin to serve in the side channel |
span_serve_floor | 0.35 | report_refusal retry floor: minimum span–query cosine to return a retry payload (up to 2 attributed [source excerpt] lines, or None) |
lossy_threshold | 2 | Span serves + raw fallbacks against one unit before it's marked lossy → append-only repair on its next rebuild touch (claims are never lost while the source hash is unchanged) |
query_keys | False | Behavioral alternate keys from confirmed rescues. Requires read_path="pool" — a loud ValueError at construction otherwise |
key_floor | 0.85 | Minimum key–query cosine for a key to count at all — below it the key row is ignored entirely. Raise it in dense same-topic corpora: sibling-article collisions are a measured watch item |
report_refusal and report_success never raise: an unknown or aged-out read_id (64-read ring), a disarmed stack, or no qualifying span is a silent None / no-op. Neither makes a retrieval or LLM call; the only side effect is report_success persisting the confirmed key when a store is configured.
The failure chain (v0.7, default-OFF)
New in v0.7 Around the pool ladder sits a second, deeper set of default-OFF triggers — the failure chain: an observe-only gap_detector that banks repair candidates on every read (serving byte-identical ON vs OFF, pinned), subs=/constraints= for a planner to feed the read, and the explicit verbs your app calls when its evaluator says a read failed — repair(read_id) (append-only, permanent), reprobe(read_id), serve_unserved(read_id). None of it runs unless armed, and arming any of it off the pool path fails loud. Full reference: The failure chain; integration recipes: your failure handling.
Other hooks on the read path
These sit alongside the ladders rather than on them:
route_by_claim(default off; unit path) — route the match on the best per-claim embedding instead of the unit's topic embedding; useful when one unit holds many distinct facts.relevance_gate— a(query, chunks) -> chunkscallable that drops irrelevant retrieved chunks before synthesis on either path, so a new unit is built from cleaner evidence.depth— on theLLMSynthesizer(0.0terse →0.5balanced default →1.0exhaustive); trades synthesis cost against how often later reads escalate. See The Synthesizer.calibrate_thresholds(embedder, positives, negatives)— derivehit_threshold/coverage_floorfrom labeled(query, understanding)pairs for a custom ortext-embedding-3-largeembedder; the shipped embedders are auto-calibrated already.
Which knob for which workload
| Workload | Reach for |
|---|---|
| Multi-hop — facts split across sources | preset="multi_hop" New in v0.5 — arms recall + the hop-2 bridge with calibrated thresholds (replaces the manual recall_threshold ≈ 0.7) |
| Real-document corpora / cold starts | widen_chunks New in v0.5 — build from the whole source, not the retrieval keyhole |
| Long-running caches | provenance_admission + adaptive_hit New in v0.5 — no duplicate builds; a hit bar that survives cache growth |
| Token-budgeted serving | serve_budget — the pool path (the default) packs to it; serve="pool" (the v0.5 preview) is deprecated |
| Per-source / attribution-heavy questions | meta= at ingest New in v0.7 or a pool_header callable — the measured 0.64 → 0.68 → 0.73 ladder |
| Contradiction / collision-heavy corpora (unit path) | hit_margin > 0 — costs rebuilds; leave off on clean data |
| Paraphrase-heavy queries over large units | the pool path (the default) — select_floor (its unit-path ancestor) is deprecated since v0.5 |
| Refusal-heavy answer loops / extraction tails | residual_spans=True + wire report_refusal/report_success New in v0.6 — refusals −33% measured, zero newly-wrong answers |
| Repeat traffic on paraphrased questions | query_keys=True New in v0.6 — keyed-class first-pass 0% → 61% (small n); latency/round-trip compounding, not an accuracy lever on diverse rewordings |
| An answer loop with its own evaluator / failure policy | gap_detector=True + wire repair to your failure event New in v0.7 — the read reports its own holes, and a failure heals the store permanently. See The failure chain |
| Extraction misses that keep recurring on the same corpus | repair(read_id) New in v0.7 — span-anchored re-extraction, append-only with per-claim provenance; paid once, first-pass forever |
| Messy real-world prose, numbers buried in text | residual_floor — recover extractor-missed number spans |
| High-stakes ambiguity | coverage_scorer (S2) — containment-grade check in the borderline band, on either path |
| Reproduce exact v0.3 behaviour | extract=False + cross_unit_recall=False |
hit_margin and coverage_scorer cost you something — extra rebuilds and an extra check respectively — so they earn their place only on ambiguous or collision-heavy data. On clean, structured corpora the pure-cosine defaults are already at parity; don't turn rungs up you don't need.
Reading the ladder in production
s = cache.stats()
s["hit_rate"] # share of reads served from cache
s["escalation_rate"] # share that hit the RAG floor
# the pool dashboard (the default path):
s["serve_gate_effective"], s["pool_noise_ceiling"] # the gate actually applied
s["pool_serves"], s["probe_reads"], s["admission_reuses"]
s["rebuilds_by_read"], s["builds_by_gap"] # why builds happened
s["pool_claims_fresh"], s["pool_claims_total"], s["pool_mask_rate"]
s["avg_units_per_serve"], s["pool_scan_slow"]
# legacy unit-path thresholds (read_path="unit"):
s["hit_threshold"], s["hit_margin"], s["coverage_floor"]
s["recall_threshold"], s["select_floor"], s["residual_floor"]
cache.stats() reports hit_rate and escalation_rate alongside the effective operating thresholds — so you can see which rung is doing the work and confirm exactly which knobs are live. A climbing escalation_rate means understanding is too thin (raise depth); a low hit_rate on paraphrases of the same question means the match bar is too high (hit_threshold, unit path). On the pool path, cache_hit means zero synthesis calls ran — probe_reads counts reads that retrieved without building (the gate-miss dashboard), and a climbing builds_by_gap with flat accuracy means the serve gate is set too high.
Next
- Context intelligence — the pool payload contract, the
pool_headergolden path, and the behavioral stack in detail. - The Synthesizer — where extractive understanding and
depthcome from. - get() & Result — the single read API and every field it returns.