Skip to content

Evidence-gated rearchitecture: measured retrieval/synthesis overhaul, eval harness, code-review fix-set - #951

Open
PromtEngineer wants to merge 30 commits into
mainfrom
rearchitect/evidence-gated-aug-2026
Open

Evidence-gated rearchitecture: measured retrieval/synthesis overhaul, eval harness, code-review fix-set#951
PromtEngineer wants to merge 30 commits into
mainfrom
rearchitect/evidence-gated-aug-2026

Conversation

@PromtEngineer

Copy link
Copy Markdown
Owner

What this is

The complete August 2026 evidence-gated rearchitecture of localGPT: 28 commits in which every behavioral change was gated on measurement — a permanent eval harness with five benchmark corpora, blind 3-voter Claude Sonnet judge panels for direction-deciding cells, and decision records for every adopt/revert call (see eval/decisions/, 15 records).

Headline results

RFC bench (24 hard questions over 23 real IETF RFCs, unseen during development): 5/24 → 19/24 Sonnet-judged, with −14% query latency. Authored benches (acq/atlas7/hr/docs, 96 rows): 78/96 deterministic-judge, up from a 65/96-equivalent baseline. New multi-turn conversation bench: 12/12.

Major changes (each with its decision record)

Retrieval & synthesis pipeline

  • Cross-leg dedupe + 12k-token synthesis context budget (fixed silent front-truncation that discarded top-ranked evidence: 5→16/24 jump)
  • Qwen3-Reranker-4B on by default as final-stage selection (union-of-max threshold 0.5, per-sub-query floor)
  • Pooled decomposition: per-sub-query retrieval → single rerank → single synthesis (composer eliminated from the default path; 24 vs 32 LLM calls)
  • Strict snippet-only synthesis prompt, temperature 0 everywhere, source-document labels (attribution failures fixed: +4 rows, zero losses)
  • Two-variant query decomposer: multi-turn variant sees the last assistant answer (fixes wrong-entity pronoun resolution); single-turn prompt frozen byte-exact with a stability gate proving decompositions unchanged on all 120 gold queries

Bug fixes from an adversarial code review (all verified, then measured)

  • split_markdown infinite-loop; docling tree-walk rewritten against the real docling_core API (indexes were structurally collapsed: acq corpus 13→82 chunks after fix)
  • Late-chunk embeddings past 8192 tokens were identical garbage vectors — now sliding-window over the full document; _lc tables get FTS indexes (hybrid leg had been silently dense-only everywhere)
  • Per-request config overrides no longer permanently mutate the shared pipeline; UI defaults realigned so the measured quality actually reaches browser users
  • SQL-injection hole in context expansion closed (refuse-don't-escape); SQLite foreign keys actually enforced; re-indexing replaces instead of duplicating rows
  • Verifier/triage/judge all pinned to temperature 0 (the 4b judge was flipping 11/24 verdicts on byte-identical answers)
  • 502/504 error semantics, upload size cap, SSE error handling, session index-state leaks, quick-chat persistence

Infra

  • eval/: 5 gold sets (120 single-turn rows + 12 multi-turn conversations), judge with validated Sonnet-subagent protocol, smoke test with port-collision guard, decision records
  • CI (gateway routing suite, 155 cases), dead code removed, docs synced to shipped behavior, chat_data.db untracked

Honest ledger

The final fix-set traded rfc 21→19 (two crossref rows lost to candidate redistribution, three long-standing losses recovered) while authored rose +4 — kept because reverting correctness fixes to protect bench rows is reverse bench-tuning. Known residue, all documented: rfc crossref rows q06/q24 (extractor patterns, plan item 1.8), hr h08/h13 (qualifier-omission style), scanned/OCR corpus eval still missing.

Verification

  • 155/155 gateway routing tests; smoke 25/25; tsc --noEmit clean (build gate re-enabled); all eval numbers reproducible from committed harnesses (rfc E2E runner adaptation pending — noted in eval/decisions/fixset-impact-2026-08-17.md)

🤖 Generated with Claude Code

PromtEngineer and others added 30 commits August 9, 2026 09:37
Full rebuild driven by a verified audit of ~180 doc/code discrepancies and
52 dead-code findings, then hardened by a browser-driven end-to-end test
and an evidence-gated adoption process (see eval/ and Documentation/research/
in the follow-up commits).

Core rearchitecture:
- Single RAG API server (api_server_with_progress.py deleted); factory.py is
  the only factory; main.py is master config + a thin working CLI
- Backend gateway reads RAG_API_URL; every documented env var is actually read
  (GENERATION_MODEL, ENRICHMENT_MODEL, EMBEDDING_MODEL, RERANKER_MODEL,
  LANCEDB_PATH, DB_PATH, NEXT_PUBLIC_*); Docker wiring fixed end to end
- Hybrid retrieval = LanceDB FTS + dense fused by RRF; retrieval_mode honored;
  no-op knobs (denseWeight, chunkOverlap, BM25 config, vision path) removed
- Streamed turns persisted via POST /sessions/<id>/messages/save with sources
  and the pipeline step cascade in message metadata; backend is the sole
  writer of chat rows
- Failed document conversion is a hard error, never a silent empty index;
  index status advances created->built; thinking-model JSON calls fixed
  (top-level think:false — chat_template_kwargs is ignored by /api/generate)

Evidence-gated defaults (measurements in eval/DECISIONS.md):
- Embedder: microsoft/harrier-oss-v1-0.6b with query-side instruction prefix
  (mixed-corpus first-stage nDCG@10 0.915 vs 0.875 for the 8GB Qwen3-4B)
- Default profile reranking OFF: bge-reranker-v2-m3 measured net-negative on
  this first stage; Qwen3-Reranker-4B (0.977) is the lazy opt-in via a custom
  scorer (rerankers 0.10.0 silently mis-scores Qwen3 rerankers)
- Per-table embedder-identity markers + L2 normalization (text_pages_v4);
  same-width model swaps now refuse instead of corrupting
- Gateway routing is a deterministic gate (~750ms/message saved; 155/155
  regression tests); evidence-sufficiency retrieval retry; decomposition moved
  to the rerank stage; VERIFIER_MODEL seam; graph module removed

Dead code removed throughout (frontend components, dup requirements, broken
scripts); all Documentation/ rewritten to describe only shipped behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cisions

Phase 0 of Documentation/research_roadmap.md — the gate everything else was
measured against:
- eval/goldset: 72 reverse-generated queries over three corpora (two
  planted-fact PDFs + Documentation/*.md), labeled by answer-bearing text so
  they survive re-chunking and embedder swaps; per-row verification records
- eval/run_eval.py: in-process recall@5/10/20 + nDCG@10 through the shipped
  RetrievalPipeline path (first stage, retry, rerank), deterministic, with
  --embedder/--reranker/--retry/--decompose flags for A/Bs
- eval/judge.py: binary groundedness judge validated on 20 hand-built cases
- eval/smoke_e2e.py: scripted end-to-end (servers, index, chat, persistence
  round-trip), 25/25 assertions, clean teardown
- eval/BASELINE.md + eval/DECISIONS.md + eval/decisions/*: every measurement,
  every adoption call, and the gate corrections — including the three cases
  where this harness overrode the published literature

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Documentation/research/: three primary-source sweeps of agentic-retrieval
  SOTA as of Aug 2026 (industry, academic, component map), each claim graded
  established/emerging/contested with rejection logs and could-not-verify
  appendices. Evidence about the field, not claims about this repo.
- Documentation/research_roadmap.md: the phased, eval-gated plan that turned
  that evidence into the shipped defaults; Phases 0-3 complete with honest
  outcome notes (including retracted/corrected evidence claims)
- Documentation/design_rationale.md: per-component what/why/what-would-change,
  citing evidence and eval numbers, plus the deliberately-not-implemented list

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rch ideas

Six mechanisms from PromtEngineer/agentic-file-search mapped onto the
retrieval-first architecture as escalation tiers (full-document escalation,
cross-reference hop, overview prefilter, metadata filter DSL, token tracking,
ephemeral ask-a-folder), with the loop/scan patterns the 2026 evidence
deprecates explicitly excluded. All items flag-gated and benchmark-gated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…asurement

Implemented behind flags, benchmarked on/off, and set per the numbers
(evidence: eval/decisions/phase4-*.md):

- 4.5 per-query token tracking (ON): ContextVar TokenUsageTracker in
  ollama_client; token_usage per stage on /chat and the SSE complete event
- 4.4 metadata filter DSL (no flag, inert unless a filters object is sent):
  rag_system/retrieval/filters.py compiles JSON filters to LanceDB where-
  clauses prefiltering both search legs; quoting characters refused, never
  escaped; gateway forwards filters and treats one as force-RAG
- 4.6 ask-a-folder CLI: ephemeral index, standard pipeline, cleanup verified
  including on SIGTERM (rag_system/ask_folder.py)
- 4.2 crossref extraction (ON, index-inert regex incl. numeric-prefix-stripped
  filename aliases); query-time hop REJECTED as a default (flag kept, OFF):
  0/11 fires at shipped k=20, 0/11 target precision where forced, beaten by
  raising k at equal context budget
- 4.1 full-document escalation (OFF, HOLD): judged lift on fired queries is
  confounded with the serving-side ~8k silent prompt front-truncation; the one
  product-default fire regressed
- 4.3 overview prefilter (OFF): boost helps only heterogeneous corpora;
  restrict rejected (removes answer documents from recall@20)

Also: api_server --port is honored; escalation subclass hooks documented as
load-bearing in retrieval_pipeline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- acquisition corpus (10 M&A PDFs), 24 gold rows (11 requires_crossref),
  100 planted facts with 54 cross-references, verify_crossref_goldset.py
- run_eval.py: final-candidate-list metrics (recall_final@k, ndcg10_final)
  alongside first-stage - the crossref hop appends to `documents`, so scoring
  first_stage alone reports a flat line by construction; invariant check
  final==first_stage when rerank+hop are off; --crossref-hop /
  --overview-prefilter / --overviews / --k toggles; hop-precision columns
- BASELINE.md: rebuilt-index baselines (post resolver fix) + determinism
  protocol; zero drift vs tracked numbers, invariant green on all corpora
- six Phase-4 decision files: escalation+tokens, crossref+prefilter (with the
  gate-correction appendix on the resolver fix), filters+ask-folder, the
  eval-metric wave's zero-hop root-cause, retrieval benchmarks (12 arms +
  budget-matched controls), answer-quality A/Bs (judged, with the 8k
  truncation confound and judge-noise findings)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- research_roadmap.md: Phase 4 marked complete with per-item verdict table;
  4.4 row corrected (page/date filters did not ship)
- design_rationale.md: new 5a (token accounting), 5b (filters + ask-a-folder),
  13a (implemented-but-disabled-by-measurement: escalation HOLD, hop REJECTED
  as default, prefilter boost HOLD / restrict REJECTED, with the deciding
  numbers)
- api_reference.md: token_usage on /chat and the complete event; filters on
  both chat request shapes with the validation contract; retrieval_retry /
  crossref_hop / document_escalation SSE events documented (retrieval_retry
  was a pre-existing parity gap); steps? on /messages/save
- improvement_plan.md: Phase-4 rows graduated to Landed; new backlog: context-
  window budgeting (verified 8194-token silent front-truncation), query-aware
  hop targets, FTS sub-query sanitization, page/date columns, token_usage UI
  + gateway passthrough, judge/smoke nondeterminism notes

Gold-set integrity after these edits: 176/176 planted facts verified,
all goldset row checks pass, smoke 25/25.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ollama_client never set options.num_ctx, so every call inherited the
server's split-slot ceiling (measured 8194 prompt tokens on this host)
and Ollama silently dropped the FRONT of oversized prompts — deleting
the top-ranked evidence from synthesis prompts with no error.

Both clients now request a window sized to the prompt, bucketed
8k/16k/32k with a per-model monotonic ratchet: changing num_ctx forces
a KV-cache reallocation, and the agent's alternating small/large calls
made naive bucketing 2.5x slower (smoke 924s vs 363s pre-fix); the
ratchet only grows the window, running the suite in 208s. A response
whose prompt_eval_count fills its window logs a truncation warning.
OLLAMA_NUM_CTX pins an exact value; OLLAMA_NUM_CTX_MAX caps the bucket
(default 32768).

Verified: the 99k-char probe that returned prompt_eval_count 8194 and
lost a fact planted at position 0 now evaluates 17,351 tokens and
recovers it; smoke 25/25 (one known q4 temp-1.0 flake mid-sequence,
rerun green). Unblocks the roadmap 4.1 escalation re-run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Executes the condition phase4-answer-quality.md §9 set for the HOLD:
re-run the exact A/B once prompts fit. They fit (249 calls, zero
truncation warnings, max prompt_eval_count 27516/32768) and the lift
did not survive — it inverted. The escalation-off baseline on the
identical acqdocs fire subset went 0/9 -> 7/9; on fired rows the
direction is now 5/7 -> 2/7 mechanically and 5/7 -> 5/7 by hand
adjudication (the four mechanical HARM rows are judge artifacts whose
verdicts contradict their own reasons). Both fires under the true
product default were regressions on both dates. The 2026-08-09 lift
was front-truncation favoring the tail-appended document, exactly as
§6.2 hypothesized.

Verdict: 4.1 REJECTED as a shipped default (was HOLD); flag and code
kept. New decision record eval/decisions/phase4-escalation-rerun.md
(agent-run, gate-validated: all cells recounted from raw JSONL);
§4 of phase4-answer-quality.md marked VOID (its 0/9 baseline was a
truncated-context artifact); verdict tables updated in
research_roadmap.md, design_rationale.md §13a, improvement_plan.md.
New backlog: judge verdicts contradicting their own reasons (strip the
verifier suffix before judging), and a 35k-char verbatim-transcription
failure mode when whole documents enter an untruncated window.

Gold-set integrity re-verified after the doc edits: 176/176 facts,
all acquisition row-level checks pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rd-case benchmark

The Phase-4 A/Bs showed the qwen3.5:4b judge returning verdicts its own
reasons contradict on exactly the rows that decide feature adoption, with
one reason citing the verifier's appended "[Confidence: N%]" suffix as
grounds for rejection (eval/decisions/phase4-escalation-rerun.md §6).

Three changes, all eval-only (the serving path stays fully local):

1. judge.py strips the verifier suffix from both judge slots before
   scoring. Measured effect on the 18 hand-adjudicated hard rows: the 4b
   goes 13/18 (k=5, unstripped) -> 15/18 (k=1, stripped) — the suffix was
   a real perturbation source, fixing 3 of the 4b's 5 known-wrong rows.
2. GroundednessJudge routes any model named claude-* through the
   anthropic SDK (lazy import; JSON shape enforced server-side via
   output_config json_schema; refusal-aware). Select per run with
   JUDGE_MODEL=claude-sonnet-5. Not validated live yet — no API
   credentials on this machine; validation commands are in the
   validate_judge_hard.py docstring.
3. The re-run's 18 manually-labeled rows are preserved as a permanent
   judge benchmark: eval/judge_hard_cases.jsonl (answers + gold + labels
   + the 4b's recorded votes) and eval/validate_judge_hard.py (k-vote
   scorer; exits nonzero unless the candidate beats the 13/18 baseline).

Phase-0 20-case validation re-run after the change: 19/20 (0.95, gate
>=0.90). The one flip is unrelated to the edit — suffix stripping is a
no-op on that case's text; it is the documented temp-1.0 paraphrase
noise ("indicates pump cavitation" vs "Pump cavitation detected").
Gold set re-verified after the doc edit: 176/176.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…plits

No API key is available on this machine, so validation ran through three
independent Claude Sonnet subagents instead of the API backend: each voter
executed the exact v1 judge prompt (verifier suffix stripped, byte-identical
to what eval/judge.py sends) over all 38 cases in isolation.

Result: 20/20 on the Phase-0 validation set (TPR 1.0, TNR 1.0) and 18/18 on
the hard-case set, with zero non-unanimous rows across 114 judgments. That
includes all 5 rows the qwen3.5:4b k=5 majority got wrong, and the
fact-present-but-prefaced-with-a-denial answer (acq_q09) that even the
suffix-stripped 4b failed in both arms. Verdict reasons were spot-checked
for substantive evidence citation (not leakage; voters never read labels).

Baselines on the same hard set: 4b unstripped k=5 = 13/18; 4b stripped
k=1 = 15/18; Sonnet subagents k=3 = 18/18.

Record: eval/decisions/judge-sonnet-validation-2026-08-13.json (per-case
votes). improvement_plan.md documents the protocol: direction-deciding
cells use 3 Sonnet subagent voters, majority decides; the 4b stays as the
free bulk-pass judge. Gold set re-verified after the doc edit: 176/176.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…back

Two known product bugs from the Phase-4 backlog (improvement_plan §1.7, §6):

1. The streaming synthesis paths never set think, so the 9b generation
   model defaulted to thinking and could burn its context window on
   chain-of-thought that never enters `response`, returning an empty
   answer (measured: prompt 9351 + thinking 7033 = window exactly).
   All three sites — RetrievalPipeline._synthesize_final_answer and both
   agent-loop compose streams — now pass enable_thinking=False, verified
   at the wire (think:false present on the captured Ollama payload).
   Side effect: the E2E smoke suite dropped from 208s to 60s, since
   every synthesis call was previously paying for hidden CoT tokens.

2. A decomposer sub-query wrapped in double quotes made the LanceDB FTS
   parser raise ("position is not found but required for phrase
   queries") and the whole hybrid retrieve() returned nothing instead
   of using the healthy dense leg (1/24 gold queries in the Phase-4
   A/B). retrievers.py now strips double quotes before the FTS leg, and
   a hybrid FTS-leg failure of any kind degrades to dense-only with a
   warning; fts_only mode still propagates. Verified against the exact
   incident query from phase4-answer-quality.md §3.2: 0 docs -> 5 docs
   with the answer document ranked first; degradation exercised with a
   simulated FTS failure; filtered-search behavior unchanged.

Smoke: 25/25 (60.2s wall).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MarkdownRecursiveChunker._split_text reassembled re.split's captured
separators with a loop that consumed two elements but advanced three,
silently discarding the first and every alternate body segment whenever
a document exceeded max_chunk_size on a separator pass. Long documents
entered the index at ~50% content: RFC 9000 retained 49.12%,
design_rationale.md 49%. The authored eval corpora (tiny acq PDFs,
mostly-small docs files) never made the loss visible; the unseen-corpus
RFC shakedown did — 10 of 24 gold answers were absent from the index.

The loop now keeps segment 0 and reattaches each separator to the
segment that follows it. Verified: 30 randomized structured documents
round-trip with zero non-whitespace loss through the split stage; the
6-section synthetic repro retains all sections (was dropping 2); RFC
corpus worst-file retention 94.3% and design_rationale.md 99.8%, with
the remainder being whitespace normalization only. Smoke 25/25.

Consequence recorded in improvement_plan.md: every index built before
this fix under-contains its long documents and should be rebuilt;
pre-fix docs-corpus baselines are not comparable to post-fix numbers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… + gold set

First eval corpus not authored by us: 23 IETF RFCs (QUIC/HTTP-3 family
plus dependencies), 1.44 MiB, 110 directed intra-corpus references,
every document connected to >=4 others. Downloaded verbatim from
rfc-editor.org; MANIFEST.md records each file's URL and role;
download.py re-fetches and verifies the corpus (--check validates the
reference graph).

Gold set: 24 hand-authored rows (16 factoid / 3 negative / 3 procedural
/ 2 comparative; 10 requires_crossref, 2 multi-document), every
expected string mechanically verified verbatim-present in its named
source by eval/verify_rfc_goldset.py (26/26 fact checks, 24/24 flag
consistency; gates re-run at the gate independently).

This corpus is what exposed the chunker data-loss bug (7d71051) and
the crossref extractor's 0/1403 resolution rate on naming conventions
we did not author. Shakedown results land in a follow-up commit once
the Sonnet judge panel completes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sis is the bottleneck

Post-chunker-fix results on the 23-RFC unseen corpus, gate-validated:

- Index: 683 chunks (was 387 — the recovered half), 51.6 min with
  enrichment, zero truncation warnings. Gold reachability 24/24
  (was 14/24).
- Retrieval (--retry off): recall@5/10/20 = 0.750/0.833/0.958,
  nDCG@10 0.659 (crossref slice 1.000 recall@20 / 0.719 nDCG).
  Live gate reproduction matched recall@20 exactly and the rest
  within one query. Versus the authored corpora (0.958 R@10 /
  0.738 nDCG) unseen-real is harder by a same-domain-distractor
  margin, not broken. Retry fires 1/24.
- E2E answers, judged by the validated Sonnet subagent panel
  (3 voters, all 24 rows unanimous): 5/24 contain the gold fact —
  single-doc 1/14, crossref 4/10. The 4b k=5 agreed 22/24; both
  disagreements were rows it had self-flagged as suspect.
- Crossref extraction: 0/1403 resolved on real RFC naming — filenames
  absent from prose, bracketed section citations discarded by
  _SECTION_RE, no RFC-NNNN pattern. Filed as item 1.8.
- New bottleneck filed as item 1.9: with retrieval fixed, the 9b
  synthesizes from its prior on dense technical text and fabricates
  citations (e.g. a invented quote from "RFC 9002 §13.4" while the
  correct value sat in the retrieved snippets); the verifier flags
  low confidence but the wrong claim still leads.

Full record: eval/decisions/rfc-shakedown-2026-08-13.md (+ per-row
Sonnet votes). Gold set re-verified after doc edits: 176/176.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… reject 35b

Five-arm A/B on the unseen 24-question RFC corpus, all arms judged by
the validated Sonnet subagent panel (3 blind voters per arm); full
record in eval/decisions/synthesis-grounding-ab-2026-08-13.md:

  A  shipped prompt, qwen3.5:9b            5/24  (baseline)
  B  strict prompt,  qwen3.5:9b            6/24
  C  strict prompt,  qwen3.5:9b, temp 0    7/24  (zero judge splits)
  D  strict prompt,  qwen3.6:35b-a3b       4/24
  D2 shipped prompt, qwen3.6:35b-a3b       5/24

Landed (arm C's exact tested config): the synthesis prompt loses its
"General knowledge" escape hatch — the hole every observed
fabricated-citation failure went through — and forbids quotes, section
numbers and document names not present in the snippets; synthesis and
the agent's compose stream decode at temperature 0 (stream_completion
gained an `options` kwarg, wire-verified). Stated honestly: the +2 is
at the harness noise floor and is not claimed as a measured quality
win — adoption rests on escape-hatch removal, decode determinism
(targets the documented temp-1.0 smoke/eval flake), and zero cost.

Bigger model REJECTED: qwen3.6:35b-a3b measures equal-or-worse under
both prompts — grounding on dense unseen text is not parameter count
at this scale. (It is ~2x faster per query and stays a viable
GENERATION_MODEL env swap for speed on large-RAM machines.)

Item 1.9 stays open; the identified next lever is the compose gap
(the composer writes the judged answer on 4-9/24 rows and carries no
grounding rules; the mandated abstain sentence was emitted 0 times in
120 answers).

Smoke 25/25 (first attempt failed on an unrelated stray
`python -m http.server 8000` squatting the gateway port; killed).
Gold set re-verified after doc edits: 176/176.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…p arm C

Single-variable test of the compose-gap hypothesis: the composer prompt
got the same hard grounding rules as synthesis. Sonnet panel: 4/24 vs
arm C's 7/24. Attribution is clean — compose fired on 8/24 rows and all
three pass->fail flips vs C were composed rows (0 gains among them);
the strict copy-contract that works against raw snippets drops facts
when applied to already-synthesized sub-answer prose. Change reverted;
shipped config remains exactly arm C. Decision-file appendix records
the negative result and the remaining 1.9 levers (abstain-on-low-
verifier-confidence, deterministic decomposition, passage citations).

Gold set re-verified after doc edit: 176/176.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause, measured: synthesis prompts were ~94k tokens ("512-token"
chunks store at ~823 tokens with enrichment prefixes; two retrieval legs
returned 20+20 with no dedupe; the +-1 sibling merge tripled each entry)
while Ollama's parallel-slot split served ~16k and silently
front-truncated — deleting the TOP-ranked evidence on every call.

- retrieval_pipeline: dedupe across base + late-chunk legs on
  (document_id, chunk_index); new _budget_synthesis_context() packs
  rank-ordered docs into an explicit token budget (default 12k,
  synthesis_context_tokens config) with sibling-span overlap
  suppression and a min-one-doc guarantee.
- ollama_client: slot-proof front-truncation warning
  (prompt_eval_count < prompt_chars // 6 catches the served-window
  split the num_ctx comparison was blind to).
- .env.example: OLLAMA_NUM_PARALLEL=1 note.

Arm F E2E on the unseen RFC corpus (3-voter blind Sonnet panel,
2-of-3 majority, verifier suffix stripped): 16/24 judged grounded
(single-doc 10/14, crossref 6/10) vs shipped arm C's 7/24 (2/14,
5/10). One split across 72 votes. Mechanics: context 335k -> ~40k
chars, truncation warnings 32 -> 0, cited-expected-source 24/24,
wall time -32%. Per-row votes in synthesis-ab-arm-f-panel.json;
narrative in the decision file's arm F appendix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rm G)

With the 12k budget controlling how MUCH context synthesis gets, the
reranker now controls WHICH and HOW MANY docs get in — threshold
selection, not just reordering (user-directed design):

- reranker.min_score (default 0.5): union-of-max across the original
  query + sub-queries — a candidate survives if the calibrated Qwen
  scorer marks it relevant to ANY of them. min_keep floors the
  selection at 3; top_k 10 caps it; the token budget stays as backstop.
  Threshold applies only to the Qwen scorer class (calibrated
  P(relevant)); raw cross-encoder logits have no fixed scale.
- Candidates are scored on their core chunk text (preserved in
  metadata.core_text at latechunk-merge time), not the +-1-merged
  block — the merge buries the matching chunk past the scorer's
  2,048-token truncation window.
- Default profile: enabled: true with Qwen/Qwen3-Reranker-4B.
  Supersedes the Phase-1 off-by-default call, which predated the
  context budget (rank order barely mattered under front-truncation).

Arm G E2E on the unseen RFC corpus (3-voter blind Sonnet panel):
18/24 grounded (single-doc 11/14, crossref 7/10) vs arm F's 16/24
(10/14, 6/10); gains q02/q17/q22, loss q20 (2-1 judge-nuance split).
Net +2 is within the 1-2 row noise floor — adopted as a no-regression
user-directed feature, not a claimed quality win. Selection is
genuinely adaptive: mean 8.8 docs kept, range 3-10 (one query kept
3/36). Cost: median query 45s -> 71s (+92% total wall) from the 4B
scorer on MPS; opt-out reranker.enabled: false restores arm F.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n shape)

Decomposed queries used to run one full pipeline per sub-query (N rerank
passes, N synthesis calls) and compose the sub-ANSWERS. Now they pool:

- _pooled_first_stage(): per-sub-query first-stage retrieval, pooled and
  deduped on (document_id, chunk_index), round-robin interleaved, each
  candidate tagged with its source sub-queries.
- One rerank pass scores each candidate ONLY against the sub-queries
  that retrieved it (union-of-max, same pair cost as the old per-SQ
  passes), with a per-sub-query floor so no sub-question's evidence can
  be entirely thresholded out.
- One synthesis against the original question. The composer — where
  arm E measured fact loss — drops out of the decomposition path.
  Config: query_decomposition.{compose_from_sub_answers: false,
  pooled_first_stage: true}.

QueryDecomposer now decodes greedily (temperature 0, via a new options
kwarg on generate_completion; wire-verified): identical splits and
sub-query text across repeat runs, ending the temp-1.0 row-set drift
that had muddied every synthesis A/B.

Measured on the unseen RFC corpus, 3-voter blind Sonnet panels:
first run (sampled decomposition) 17/24 vs arm G's 18/24 with only one
pooling-attributable flip; per user decision, determinism was fixed and
both paths re-run on IDENTICAL row sets (6 decomposed queries, verbatim
same sub-queries): pooled 17/24 vs composer 17/24 — dead tie, zero
split votes across 144 judgments, and all 18 direct-path rows judged
identically across arms. Adopted on structure: 24 vs 32 synthesis
calls, linear (not multiplicative) scaling with sub-query count, one
less failure surface. Panel records + decision-file appendix included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diagnosis of the surviving crossref failures showed half were
attribution failures, not fact failures: answers with every salt,
parameter name and identifier character-for-character correct were
judged ungrounded only because they could not name the defining RFC.
Root cause was ours — the synthesis context was a bare join of chunk
texts with no source labels, while strict-prompt rule 3 (correctly)
forbids writing document names not present in the snippets. RFC
corpora cross-reference by tag ("[QUIC-TLS]"), so the model had no
grounded path to "RFC 9001" even though the pipeline knows every
chunk's source file.

Every snippet in the synthesis context now opens with
"[Source document: <document_id>]", and new prompt rule 7 tells the
model to attribute facts via those labels when sourcing matters.
Grounding stays strict: document names are now IN the snippets.

Arm I on the unseen RFC corpus (3-voter blind Sonnet panel, identical
deterministic row set to arm H2): 21/24 grounded (single-doc 12/14,
crossref 9/10) vs H2's 17/24 (11/14, 6/10) — gains q06/q18/q20/q24,
ZERO losses, one split. +4 with no regressions is well outside the
established 1-2 row noise floor. RFC arc: 5 -> 7 -> 16 -> 17-18 -> 21.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…agnosed

HEAD (a3f999a) vs pre-tuning baseline (80d5215) on all 96 authored gold
rows, identical indexes: acq 16->18 (4b), atlas7 19->19 (4b), docs 15->17
(Sonnet panels, 0 splits), hr 24->21 (Sonnet panels, 0 splits — real).
Mechanical exact-substring 24/96 -> 61/96; wall time -27%. hr losses
diagnosed in-process: evidence WAS in context (2-chunk corpus, both kept);
the strict prompt answers the literal question and omits adjacent clauses
the golds include — style, not grounding. Verdict: not overfit; changes
stay; completeness-prompt tweak queued with 5-bench validation required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…get rows

Rule-8 candidate for the hr -3 regression fails its purpose: hr_h05 answer
byte-identical to the panel-rejected HEAD answer, h08/h13 still missing the
gold qualifiers. 4b screen movement (+3/96 authored, rfc 14=14 same-judge)
is noise-band churn with new qualifier losses (hr_h04, acq 18->16).
Strict prompt stays at the arm-I 7-rule form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every _lc table lacked an FTS index (indexing created one only on the base
table), so the hybrid lc leg silently ran dense-only on ALL corpora since
late-chunking landed. Arm K (120 rows, index present vs absent): E2E-neutral,
5/96 rows changed citations, Sonnet panel 0 regressions on all deciding cells.
Also: 4b screen judge sampled at default temp — 11/24 verdict flips on
byte-identical answers; GroundednessJudge now decodes at temperature 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n prompt frozen

Two-variant prompt selected on bool(chat_history). Multi-turn variant adds a
last_assistant_answer field (wrong-entity pronoun resolution measured on the
new eval/goldset/multiturn.jsonl: 'their'->StartupXYZ at baseline, MegaCorp
after) + ellipsis rule 1b (follow-ups no longer collapse into the previous
question). Single-turn variant stays byte-exact: arm L measured m1c's prompt
additions costing 2 Sonnet-confirmed rfc rows via degraded decompositions;
verified byte-identical temp-0 decompositions on all 120 single-turn gold
queries after the split. Multi-turn E2E 12/12 (arm m1d).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings addressed (all verified before commit):
- docling_chunker: split_markdown infinite loop -> index-based window with
  strict progress; element walk rewritten against real docling_core API
  (label.value/section_header/prov) — heading paths and reading order restored
- agent/loop: per-request overrides now snapshot/restore shared config
  (was sticky process-wide); triage pinned temp 0; history capped 40 turns
  with verifier tags stripped
- UI defaults realigned to shipped profile (aiRerank on, compose off) —
  arm G-L quality now actually reaches UI users
- retrievers: FTS score read from _score; retrieval_pipeline: refuse-dont-
  escape document_id in context expansion, filter passed into executor
  workers (thread-local not inherited), per-call table name
- embedders: delete-by-document_id before append (rebuilds no longer
  duplicate); latechunk: sliding windows over full doc (tail chunks got
  CLS-token garbage past 8192 tokens)
- backend: sqlite _connect() factory with FK pragma + 30s timeout; upload
  size cap; chat errors return 502/504 (was 200-with-error-text);
  chat_data.db untracked
- verifier: grounded/confidence coercion fail-open; verifier+retry+async
  client temp-pinned (options param added to generate_completion_async)
- eval: anthropic judge temp 0; run_eval realigned to arm-G profile with
  index/prompt cache versioning; smoke pre-flight port guard; rfc corpus
  registered
- frontend: session index-state clearing, regenerate dedupe, SSE error
  handling, quick-chat persistence, memoized markdown, tsc gate re-enabled
- dead code removed (legacy prompt blocks, unused endpoints/components,
  duplicate compose file); .github/workflows/ci.yml added (155-case
  gateway routing suite; passing)

Single-turn decomposer prompt verified byte-identical on all 120 gold
queries post-change (decomp stability gate). Synthesis + judge prompts
untouched. Index-affecting fixes require bench-index rebuild; measured in
the follow-up eval run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…odes, line refs)

Propagates the two landed default changes (reranker ON with threshold
selection since arm G; pooled decomposition replacing compose since arm H)
across README, system/architecture overviews, design_rationale, api_reference,
deployment/docker/quick-start guides, and eval docs. Fixes the specific
falsehoods the docs audit flagged (502/504 now true after the code fix,
lazy reranker load, per-chunk enrichment, prompt_inventory ghosts) and
drops hard line-number references.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… recovered

Full 120-row rerun on rebuilt v2 indexes (fix-set changed index content:
acq 13->82 chunks etc). Deterministic 4b: authored 74->78. Full Sonnet
panel on rfc arm M: 19/24 vs arm-I 21/24 — q06/q24 real losses, q10/q15/
q20 real gains from the corrected latechunk vectors. hr_h05 (unfixable by
prompt rules) recovered via proper chunking. Multiturn 12/12 held. Fix-set
stays: correctness over bench-row protection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…treaming

Streaming 'stopped working' root cause: the Next dev watcher shares its root
with the Python backend, so every chat_data.db write Fast-Refreshed the page
mid-stream, remounting React and aborting the in-flight SSE fetch
(net::ERR_ABORTED). next.config.ts now scopes the watcher to frontend
sources. Verified: no rebuild storms, live DOM growth during both quick-chat
and session-chat generations.

Avatars: AvatarFallback rendered black text on the dark bg-muted circle —
invisible once the third-party pravatar image was removed. ChatBubbleAvatar
fallbacks now use an explicit light scheme; user bubble shows 'U'.

Quick Chat now streams: new gateway POST /chat/stream (SSE, same
token/complete/error framing as the RAG API so the client parser is shared),
chat_stream() on the backend Ollama client, api.streamChatMessage(), and
token-by-token rendering in quick-chat.tsx. Gateway suite 155/155; tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every Markdown render sat inside a whitespace-pre-wrap container, so each
newline in the answer displayed twice — once as ReactMarkdown's paragraph
margin and once as a literal line break — producing large vertical gaps
around headings and between paragraphs. Fixes:

- drop whitespace-pre-wrap from all four Markdown-wrapping sites
- add remark-breaks so intentional single-newline breaks still render
  (the reason pre-wrap was there in the first place)
- tighten prose margins for chat bubbles (prose-p:my-2 etc.)
- make normalizeWhitespace fence-aware: whitespace inside ``` code blocks
  is no longer collapsed (it was corrupting code indentation and ASCII
  tables quoted from documents)

Verified live: prose computes white-space normal, real p/li/strong elements,
zero 3+-newline runs on the rendered page. tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant