AGENTS.md@crates/wenlan-core/src/retrieval · git:20260805.b673398 · 2026-08-05 · sha256 501d5a501c136843

AGENTS.md@crates/wenlan-core/src/retrieval git:20260805.b673398A

Immutable. This exact content is served forever at /api/v1/blob/501d5a501c136843.

# Retrieval & rerank env flags

The retrieval-channel flags, extracted from `crates/wenlan-core/AGENTS.md` so they load
when you are working under `retrieval/` rather than on every wenlan-core session. Each
entry carries its default, its wiring, and the paired-A/B receipt behind its default.

`drift_guard` teeth #2 scans every tracked `*AGENTS.md`, so the flag-doc contract holds
from here.

- `WENLAN_RERANKER_MODEL` — cross-encoder model selector for the opt-in CE rerank path (`WENLAN_RERANKER_ENABLED=1` on the daemon; CE itself stays default-OFF). DEFAULT (unset/unrecognized) = `bge-base` (BGERerankerBase, 1.1GB) since 2026-06-11: LME paired sweep PR #260 (scaffold N=1, n=479) measured it at 94% of v2-m3's NDCG lift (+0.1058 vs +0.1130 agg, both BH-sig, all categories above the A/A floor) at 29% of its marginal P50 cost (dP50 +318ms vs +1094ms; on-arm P50 510ms vs 1279ms CPU). `bge`/`bge-v2-m3` = quality ceiling for users who accept ~1.3s P50; `turbo`/`jina-turbo`/`jina` = JINARerankerV1TurboEn (146MB, +20ms P50, 71% of v2-m3's lift — the latency-critical pick). History: the 2026-06-04 LoCoMo model A/B (channels OFF, older substrate) ranked base WORST (0.500 vs turbo 0.569 vs v2-m3 0.612) — the base-vs-turbo ordering is fixture-dependent; the default decision trusts the LME sweep on the current harness/substrate per user call 2026-06-11. v2-m3-vs-base ordering is consistent on both. BYO override: `WENLAN_RERANKER_ONNX_DIR` (+ `WENLAN_RERANKER_MODEL_ID`) bypasses the selector entirely.
- `WENLAN_RERANKER_MODE` — cross-encoder activation mode: `off` (DEFAULT) | `lite` | `full` (PR-B). Selects which retrieval paths get a CE and which model, wired at the daemon HANDLER layer (never inside core `search_memory`, so internal callers `search_corrections_by_topic` + `verify_page` stay CE-free). `off` = byte-identical to no rerank. `lite` = turbo (jina-turbo, ~146MB) on the quick (`/api/search`) + context (`/api/context`) paths AND on an explicit `rerank=true` deep search. `full` = turbo on quick/context + the heavy bge-base (~1.1GB, pool-widened via `compute_rerank_fetch_pool`) on the explicit `rerank=true` deep path; the deep model loads in the BACKGROUND so startup never blocks on the download (rerank=true falls back to plain hybrid until it is ready). The `rerank` request flag on `/api/memory/search` keeps its contract (`true` = rerank when a model is wired, `false` = plain hybrid) — the mode never inverts it. Legacy `WENLAN_RERANKER_ENABLED=1` with `WENLAN_RERANKER_MODE` unset = deep-only CE using `WENLAN_RERANKER_MODEL` (exactly the pre-mode behavior, blocking startup); an explicit mode wins over the legacy switch. **Config-backed + persistent:** set it set-and-forget with `wenlan models reranker <off|lite|full>` (writes `config.json reranker_mode`, mirroring `wenlan models` / `wenlan keys`); the daemon reads it at startup. Effective-mode precedence is `WENLAN_RERANKER_MODE` env (dev/override — wins only when non-empty; an empty/unset value falls through to config, matching the daemon's other empty-as-unset knobs) > `config.json reranker_mode` (persistent user setting) > `off`. Per-path state is surfaced on `/api/status` (`reranker` = deep, `reranker_light` = quick+context, `reranker_mode`) and in `wenlan status` / `wenlan doctor`. Parsed by `reranker_mode_resolved()` (env+config resolution) / `reranker_mode_from_str()` (pure parse) + `resolve_reranker_plan()` in `crates/wenlan-core/src/reranker.rs`; the handler rerank step is `rerank_results_light()` in `crates/wenlan-core/src/db.rs`. CE model weights are a user-downloadable asset (the ~1.1GB bge-base cold download is the UX gate).
- `WENLAN_ENABLE_TEMPORAL_SOFT_BOOST` — opt-in (default OFF). Multiplicatively BOOSTS in-window dated memories by `(1 + WENLAN_TEMPORAL_BONUS)` while leaving outside-window / undated / no-cue rows neutral (×1.0, never dropped). Gentler successor to the `WENLAN_ENABLE_TEMPORAL_FILTER` hard filter; the two are mutually exclusive (soft takes precedence when both are set). Measured CLOSED-NULL on LME-S (2026-06-09, paired A/B `temporal_soft_cached` / `temporal_filter_cached` arms, n=479, event_date 100% live): n_touched 15/479 and 10/479, agg deltas inside noise, temporal-reasoning slightly negative. Attribution is structural, not substrate: `temporal_query::extract_cue` recognizes only deictic now-anchored phrasing (yesterday / last week / N days ago / since YYYY), while LME temporal-reasoning questions are event-anchored ordering/arithmetic ("which did I do first, A or B", "how many days between A and B") — the cue gate fires on ~3% of queries, and the few deictic hits compute windows against the eval wall-clock, not the 2022-23 memory dates. Re-measuring with a bigger `WENLAN_TEMPORAL_BONUS` cannot change this; the lever for the TR category is multi-anchor retrieval (query decomposition, #11), not window boosting. FUTURE NOTE (fair re-test, parked): the closure is for the flags AS-WIRED, and the A/B carries a clock-era confound — read-side cue windows anchor to the eval wall-clock (2026) while memory event_dates are 2022-23, so even the few fired rows could not land in-window, and write-side grounding (T11) anchors to `last_modified` = seed-import time, same era mismatch. A fair re-test needs era-aligned clocks: per-question `now` = the question's own timestamp on the read side, and event-clock (not import-clock) grounding on the write side. The ~3% cue fire rate is regex-only and clock-independent, so the phrasing gap dominates regardless of the confound.
- `WENLAN_TEMPORAL_BONUS` — additive bonus for the soft boost (default `0.5`). Clamped non-negative: a negative or non-finite value falls back to neutral (`0.0`) / the default so the boost can only lift a row, never demote it.
- `WENLAN_ENABLE_INTENT_LLM` — opt-in (default OFF). On the deep/expanded path (`search_memory_expanded`), the existing query-expansion LLM call emits a structured intent object `{expansions, use_graph, entities, temporal_window, subqueries}` instead of a plain array of rephrasings (one call, `temperature=0`, per-field-tolerant parse via `engine::extract_json`, fallback to the keyword `classify_query` gate on timeout / error / unparseable). Slice-1 wires only `use_graph` into the deep-path graph gate (through the `graph_override` arg on `search_memory_with_cue`); `entities`, `temporal_window`, and `subqueries` are emitted and logged only (parked: `entities`→#10 graph traversal, `temporal_window`→#13, `subqueries`→#11). DISTINCT from the shipped zero-LLM T19 `WENLAN_ENABLE_QUERY_INTENT` (channel-weight classifier, `__query_intent` baseline suffix); never reuse that flag for the LLM emitter or eval baselines confound. Wiring + scope: `search_memory_expanded` is an expansion path exercised by the eval harness and the dormant `search_memory_routed` strategy (`WENLAN_LLM_ROUTE`, default OFF); the live daemon calls `search_memory` (quick) + `search_memory_cross_rerank` (deep), so slice-1 wires and PROVES the `use_graph` signal on the expanded path, while deploying it into the live deep path is a downstream step. Reconciliation with existing prototypes (all default-OFF, all off the live path): the T7 strategy router (`retrieval/route.rs` `classify_strategy` / `parse_strategy`, `WENLAN_LLM_ROUTE`, dispatched by `search_memory_routed`) and the query-decomposition parser (`retrieval/decompose.rs` `parse_subqueries`, used by the `search_memory_decomposed` prototype); the `subqueries` field deliberately reuses `decompose.rs`'s JSON-array shape so #11 can consume it without re-contracting. Measurement caveat: the paired probe's ON arm feeds the intent object's expansions into RRF while the OFF arm feeds the legacy array-rephrasing expansions, so the A/B contrasts the whole intent pipeline against the legacy pipeline, not `use_graph` in isolation; read a positive result as enable-the-intent-pipeline and add a third arm (intent-expansions + keyword gate) to attribute the delta to routing alone. Never surfaced on the MCP `recall` tool; the daemon owns retrieval routing.
- `WENLAN_GRAPH_MEMORY_STREAM` — DEFAULT ON since 2026-06-10 (opt out with `0`/`false`/`no`/`off`; unset or any other value = ON). Live entity->memory RRF stream on the quick path (`search_memory`): query-anchored non-person entities with `memory_entities` degree <= hub cap contribute their linked memories as an extra RRF term (boost-only by default; surface-new stays opt-in). Receipt: +0.0545 NDCG@10 agg, BH-sig, paired A/B on cached canonical DBs (2026-06-09). The CE deep path skips the stream ONLY under a live rescore (`allow_graph_stream = reranker.is_none()` at the `search_memory_cross_rerank_cued` pool fetch): the stream×rerank stack measured not significant (+0.0126, p=0.17), so the stream's value is the non-rerank path; a rerank call without a configured reranker degrades to plain hybrid search WITH the stream (byte-identical to base). The skip-preference bypass keeps the stream allowed (stays byte-identical to the base `search_memory` baseline). Paired-A/B note: OFF arms must set `0` explicitly (unset = ON), and the `graph_seed` arm pins the stream to `0` on both arms (seed branch is unreachable while the stream is engaged). The `rerank_graph_stack` arm is retired — with the per-path skip, both its arms are byte-identical by construction. HAZARD: `WENLAN_GRAPH_SURFACE_NEW` (opt-in) inserts graph-only rows WITHOUT re-applying memory_type/space/source_agent filters (`get_memories_for_entities` filters only entity link + source='memory' + chunk 0), i.e. a space-scoped search can surface cross-space rows — the same disclosure class the page/episode/fact channels gate; keep it OFF on multi-space data until an overlap gate lands (follow-up). Note on k-hop interaction: with the stream engaged, the opt-in `WENLAN_ENABLE_GRAPH_KHOP` branch is unreachable on the quick path (stream early-returns before k-hop); set `WENLAN_GRAPH_MEMORY_STREAM=0` to measure k-hop in isolation, mirroring the `graph_seed` note above.
- `WENLAN_RERANK_SKIP_PREFERENCE` — opt-in (default OFF). On the cross-encoder path (`search_memory_cross_rerank_cued`), preference/recommendation-seeking queries (per `router::classify::is_preference_query` — request-form keywords like "recommend"/"any tips", vetoed by past-recall markers like "you recommended"/"remind me") bypass the CE entirely and return the base `search_memory_with_cue` ranking, byte-identical to the non-rerank baseline. History: built against an older "CE hurts single-session-preference −0.155 NDCG@10" measurement that did NOT reproduce on either current seeded substrate — paired A/Bs at n=479 measured CE *helping* SSP (+0.027 on both the canonical and the re-seeded DB) and the bypass net-negative (−0.0117 agg, BH-sig). Ships as a tested, default-OFF escape hatch, NOT a recommended setting; the CE base-vs-on case itself re-verified at +0.178 NDCG@10 agg (BH-sig, positive every category, N=2 substrates) with P50 1165ms vs 111ms base (CPU BGE-reranker-v2-m3) — the default-ON decision stays gated on the smaller-CE-model benchmark. The keyword lists were validated against the full LME-S fixture: 30/30 SSP detected, 0 false positives across the other 470 questions; generalization beyond the fixture is heuristic, same trust level as the temporal/relational keyword gates. Paired A/B arm: `rerank_skip_pref` in `paired_ab_emit` (CE path, flag toggled). The `rerank_graph_stack` arm (same test) toggled `WENLAN_GRAPH_MEMORY_STREAM` with the CE active on both arms to measure graph×rerank composition — measured NOT significant (+0.0126, p=0.17): graph_stream's +0.0545 quick-path gain is subsumed under the CE (except temporal-reasoning, +0.037), so the two levers do not stack. That arm is retired as of 2026-06-10; the code now hard-skips the stream under a live reranker (`allow_graph_stream = reranker.is_none()`), making both arms byte-identical by construction.