AGENTS.md@crates/wenlan-core · diff

git:20260805.b673398 to git:20260806.9d5eb29

1 added, 3 removed. Audit A to A.

# crates/wenlan-core
Applies to agents working under `crates/wenlan-core/`. Read alongside root `AGENTS.md`, which takes precedence on any topic not covered here.
All business logic lives here. No tauri, no axum. Framework-agnostic.
## Key Modules (`crates/wenlan-core/src/`)
Only the modules whose job is not evident from the file name. Read the directory for the rest.
| Module | Purpose |
|---|---|
| `db.rs` | `MemoryDB` — libSQL storage, vectors, chunks, hybrid search, embeddings, knowledge graph, migrations. Three search methods: `search_memory` (embedding+FTS+RRF), `search_memory_reranked` (+ LLM reranking after), `search_memory_expanded` (+ LLM query expansion before). Uses `EventEmitter` trait for UI notifications (no tauri). |
| `engine.rs` | `LlmEngine` — llama-cpp-2 wrapper, model download, inference loop, format helpers |
| `merge.rs` | Memory merging, pattern extraction, contradiction detection |
| `llm_provider.rs` | `LlmProvider` trait + `ApiProvider` (Anthropic API) + `OnDeviceProvider` shim |
| `refinery.rs` | Distill-cycle orchestration, dedup, auto-linking, consolidation |
| `post_ingest.rs` | Post-ingest enrichment (dedup check, entity linking, title enrich, recap, page growth) |
| `pages.rs` | Type definitions for the `Page` struct (synthesized wiki entries distilled from memory clusters). Actual clustering + distillation live in `db.rs` + `refinery.rs`. SQL tables are `pages`/`page_sources` (renamed from `concepts`/`concept_sources` in migration 46). |
| `access_tracker.rs` | Memory access counts + time decay |
| `contradiction.rs` | Contradiction detection |
| `context_packager.rs` | Context bundle → prompt packaging |
| `importer.rs` | File importer pipeline |
| `quality_gate.rs` | Pre-store quality gate |
| `tuning.rs` | Tuning config (distill cycles, distillation, weights) |
| `sources/` | `RawDocument`, file watchers, Obsidian importer. `RawDocument` and related types re-exported from `wenlan-types`. |
| `router/classify.rs`, `content_score.rs` | Smart router scoring helpers (non-tauri parts) |
| `config.rs` | Persistent config at `dirs::data_local_dir()/origin/config.json` (on macOS, `~/Library/Application Support/wenlan/config.json`) |
| `eval/` | Benchmark harness: LoCoMo, LongMemEval. Each benchmark has base (embedding-only), reranked (LLM rescores after search), and expanded (LLM query expansion before search) variants. Baselines under `EVAL_BASELINES_DIR` (gitignored). See `crates/wenlan-core/src/eval/AGENTS.md`. |
## Database (`MemoryDB`, `db.rs`)
> Moved from root `AGENTS.md` (index-and-pointer refactor): these are `wenlan-core` internals, loaded when working in this crate. Root keeps only the one-line architectural summary.
One libSQL database at the platform data directory (`dirs::data_local_dir()/origin/memorydb/origin_memory.db`; on macOS, `~/Library/Application Support/wenlan/memorydb/origin_memory.db`), owned by `MemoryDB` in `db.rs`:
- **Document chunks**: `chunks` table with `F32_BLOB(768)` vector column, DiskANN indexing (768-dim, BGE-Base-EN-v1.5-Q)
- **Knowledge graph**: `entities`, `relations`, `observations` tables with FK cascades
- **Full-text search**: FTS5 virtual table (`chunks_fts`) auto-synced via triggers
- **Hybrid search**: Vector similarity + FTS combined with Reciprocal Rank Fusion (RRF)
**Connection pattern**: `tokio::sync::Mutex<libsql::Connection>` — `libsql::Connection` is `Send` but not `Sync`, so it's wrapped in an async `Mutex` inside `MemoryDB`.
**Sharing pattern**: `MemoryDB` is wrapped in `Arc<MemoryDB>` at the state layer (`ServerState.db: Option<Arc<MemoryDB>>`). This lets handlers clone the `Arc` out of the `RwLock<ServerState>` guard and drop the guard before performing long-running operations.
## Events (`EventEmitter`, `events.rs`)
Instead of passing `tauri::AppHandle` into business logic, `wenlan-core` defines an `EventEmitter` trait — this keeps the crate framework-agnostic and testable with `NoopEmitter` in unit tests:
```rust
// crates/wenlan-core/src/events.rs
pub trait EventEmitter: Send + Sync {
fn emit(&self, event: &str, payload: &str) -> Result<()>;
}
pub struct NoopEmitter;
```
- The daemon uses `NoopEmitter` (no UI to notify directly)
- The desktop app (separate `wenlan-app` repo) provides a `TauriEmitter` adapter that wraps `AppHandle::emit`
- `MemoryDB::new(db_path, emitter: Arc<dyn EventEmitter>)` takes the trait object
## Enrichment parity & eval-seed contract
> Moved from root `AGENTS.md` (index-and-pointer refactor): these name `wenlan-core` internals (`run_canonical_enrichment`, `eval/seed_contract.rs`), loaded when working in this crate.
### M5 reader inventory identity
`scripts/m5-reader-sweep.py --check` owns the executable page-prose reader
contract in `docs/plans/2026-07-27-m5-reader-manifest-inventory.md`. Its stable
identity is `short/path.rs::function[#ordinal]`; never put source line numbers
back into the committed block. Lines belong only in `--json` diagnostics, and
unowned relevant callsites fail closed. The required check includes mutation
controls for identity, visibility, depth/`via`, callers, and duplicates.
### Ingest-path parity (training-serving skew)
- **All post-store enrichment goes through `wenlan_core::ingest::run_canonical_enrichment`.** It is the ONE shared path for classify + extract + `apply_enrichment` + tags (Phase 1), entity/title/page enrichment (Phase 2), and dual-pool dedup/contradiction resolution (Phase 3). The server `handle_store_memory`, the eval seed pipeline, and the importer all call it. Do NOT re-implement a subset of enrichment in any consumer.
- **Why.** The eval seed used to re-implement a divergent subset (`enrich_db_for_eval` = entity + title + page only), so every new write-time feature (importance/T8, event_date/T11+T20, episode/T2, fact-channel/T15, dual-pool/T14, summary-nodes/T18) silently lagged in the eval path and shipped merged-but-inert, re-discovered as "starved" each eval cycle. Sharing the code makes seed-vs-production fidelity hold by construction. This is the standard fix for **training-serving skew** — Google "Rules of ML", Rule #32: *"Re-use code between your training pipeline and your serving pipeline whenever possible"* → *"eliminates a source of training-serving skew."* See also 12-Factor X (dev/prod parity) and the technical-debt framing (Cunningham, OOPSLA '92): the eval shortcut was debt never repaid.
- **New write-time feature checklist.** Add it inside `run_canonical_enrichment` (not in a consumer), then add a seed-completeness assert — a contract test (Fowler, `ContractTest`) — so the seed cache fails loud when the feature's artifact is missing rather than silently absent. A flag merged without its artifact present in the seed is unmeasurable.
### Eval seed + eval read: ONE route, ONE contract (no drift)
The recurring failure mode was not any single missing artifact — it was that seeding a cached scenario DB was a *scatter of manual STEP tests* (`seed_inject_event_dates`, `seed_backfill_classify`, entity sweep, `seed_backfill_episodes`, `distill_pages`) run by memory. Miss one and a channel ships starved, then a graph/temporal A/B over it returns a null that gets misread as "the channel doesn't help" — a lie about a dead substrate, re-discovered every cycle.
- **Seed side — the ONE route.** Re-seed cached scenario DBs with the orchestrator `seed_scenario_dbs_complete` (`crates/wenlan-core/tests/eval_harness.rs`). It runs every enrichment step in the correct order (event_date inject → classify → entity/`memory_entities` sweep → episodes → distill) then asserts `SeedExpectations::complete()`. **Never hand-run the individual `seed_*` STEP tests** — they are the orchestrator's internals. Run the one route and the seed is complete *and* contract-verified by construction.
- **Contract side — teeth, not prose.** `crates/wenlan-core/src/eval/seed_contract.rs` is the single liveness contract. `SeedExpectations::complete()` hard-fails the seed when a channel's substrate is empty: `memory_entities = 0` (graph), `event_date = 0` (temporal), `pages = 0` active (page channel), plus dupes + classification from `strict()`. These are *presence* checks (`> 0`), not coverage percentages — a percentage floor rots (see the L3/coverage note), but zero links means the channel is dead, which is the bug. `strict()` stays lenient (report-only) for minimal seeds; only `complete()` has teeth.
- **Eval side — refuse, don't lie.** The SAME contract gates the consumer: every per-query eval collector calls `seed_contract::assert_feature_substrate_live(conn, feature)` at entry. A graph A/B over a DB with zero `memory_entities` (or a temporal A/B with zero `event_date`, or a page-channel A/B with zero active `pages`) **errors loud** ("EVAL REFUSED") instead of emitting a null. Producer and consumer share one contract, so neither can drift onto a dead substrate.
- **Adding a write-time channel.** Add its step to `seed_scenario_dbs_complete`, its presence floor to `SeedExpectations` (+ wire it into `assert_feature_substrate_live` if it has an A/B), and a unit test in `seed_contract.rs`. The contract — not a runbook — is what keeps the seed honest.
## Retrieval, LLM throughput & consolidation env flags
> Moved from root `AGENTS.md` (2026-06-23) per the agents.md hierarchical convention: these are wenlan-core internals, loaded only when working in this crate. `drift_guard` teeth #2 scans every tracked `*AGENTS.md`, so the `WENLAN_*` flag-doc contract still holds.
### On-device LLM throughput flags
- `WENLAN_LLM_DEVICE`: llama.cpp device policy on GPU-enabled builds: `auto` (DEFAULT), `cpu`, or a llama.cpp GPU device index. `auto` prefers a discrete GPU over an integrated GPU and accelerator, then greater free memory, then the lower stable index; this preserves macOS Metal accelerators while selecting discrete Vulkan adapters on mixed-GPU Windows systems. On Windows Vulkan, invalid selection and GPU model/context initialization failure visibly fall back by reloading the model with zero GPU layers. The effective backend/device/fallback is exposed on `/api/status`; Windows build setup and physical live-smoke steps live in `docs/windows-vulkan.md`. Linux stock builds remain CPU-only.
- `WENLAN_LLM_SLOT_BACKFILL`: continuous-batch slot backfill for the on-device LLM (`OnDeviceProvider`). **DEFAULT OFF (opt-in; enable with `1`/`true`/`yes`/`on`).** When ON, a continuous-batch call may drain more than `m` (= `WENLAN_LLM_PARALLEL_SEQS`) immediately-available requests and the engine keeps all `m` KV slots full by backfilling the next queued request the moment a slot finishes — so decode width stays at `m` instead of raggedly draining `m`→1 as short outputs (classify ~20 tok) finish before long ones (entity ~100 tok). Fixes the decode-bound enrichment throughput floor (the seed enrichment batch measured 40% prefill / 59% decode; the 59% is this ragged drain). Pure throughput optimization, semantically-equivalent outputs (same prompts, same per-request sampler seeds; cross-slot KV is cleared on reuse). The drain cap is `m * 4` (m≤8 → ≤32), bounding per-call latency since a drained batch's outputs return together. **Default-OFF** because it is a structural rewrite of the SHARED inference path that CI cannot validate on Metal; enable it for throughput-bound paths (bulk ingest, eval seed firehose) where the latency-return-together tradeoff is acceptable. When OFF (the default) the drain caps at `m` — one slot per request, byte-identical to the pre-backfill engine. The no-overflow path (queue ≤ `m`, e.g. a single memory's enrichment) never backfills regardless of the flag. Parsed by `slot_backfill_enabled()` in `crates/wenlan-core/src/llm_provider.rs`; the engine scheduling lives in `LlmEngine::run_inference_continuous_batch` (`engine.rs`, `BackfillScheduler`). Follow-ups before any default-ON flip: separate live/bulk queues (or priority gating) so live enrichment can never inherit firehose head-of-line latency, and stochastic/cancellation/timeout fuzzing beyond the GPU grounding oracle. Correctness validated by `eval::engine_throughput::tests::backfill_grounding` (KV-reuse cross-contamination oracle, L7 GPU) + the wall-clock A/B `backfill_throughput_ab`.
- `WENLAN_LLM_PREFIX_KV_CACHE`: prefill-side prefix-KV cache for the on-device LLM continuous batch (`OnDeviceProvider`). **DEFAULT OFF (opt-in; enable with `1`/`true`/`yes`/`on`).** When ON, `LlmEngine::run_inference_continuous_batch` (`engine.rs`) detects the longest token prefix shared by every sequence in the batch (the fixed chatml system prompt + task framing — ~80% of a ~225-token enrichment prompt), primes that prefix's KV **once** into seq 0, fans it to the other slots via `copy_kv_cache_seq`, and then prefills only each request's unique suffix at positions `[prefix_len, ..)`. The per-slot pre-prefill clear preserves `[0, prefix_len)` (clears only `[prefix_len, ..)`), so a slot-backfilled request reuses the resident prefix too — the prefix is encoded exactly once for the whole batch. Attacks the **prefill** half of the enrichment batch (measured 40% prefill / 59% decode via `[batch_timing]`); complementary to `WENLAN_LLM_SLOT_BACKFILL`, which attacks the decode half. Honest ceiling ~1.5x on the prefill portion, not the whole batch. **Semantically equivalent to a full per-seq prefill** — a prefix token's KV is causally closed and batch-composition-independent in exact arithmetic, so the algebra is identical; as with all batched GPU inference, intermediate logits can still differ at the ULP level (greedy decode absorbs this — the L7 equivalence oracle asserts byte-identity at temperature 0). A pure throughput optimization, not a semantic change. The cache engages only on TRANSFORMER models (gated off when `model.is_recurrent()` or `model.is_hybrid()` — e.g. Qwen3.5-9B's DeltaNet hybrid layers, whose compressed recurrent state is not the position-addressable per-token K/V that prefix sharing requires), and only when there are ≥ 2 sequences, ≥ 2 KV slots, and the shared prefix clears `PREFIX_KV_MIN_TOKENS` (32); otherwise, and on any priming failure, it falls back to byte-identical full per-seq prefill (`prefix_len = 0`). **Default-OFF** because, like slot backfill, it mutates the SHARED on-device inference path's KV handling, which CI cannot validate on Metal. Parsed by `prefix_kv_cache_enabled()`; the prefix-length seam is `reusable_prefix_len()` / `longest_common_prefix_len()` in `crates/wenlan-core/src/engine.rs` (pure, unit-tested). Correctness validated by `eval::engine_throughput` (fresh-vs-cached equivalence oracle, L7 GPU) + the `[batch_timing]` `prime_ms`/`prefill_ms` A/B.
### Background sweep & routing flags
The retrieval-channel and rerank flags moved to [`src/retrieval/AGENTS.md`](src/retrieval/AGENTS.md), which loads when working under that module. What follows gates background sweeps and compile routing.
- `WENLAN_ENABLE_ENTITY_SWEEP` — DEFAULT ON (opt out with `0`/`false`/`no`/`off`). Gates the background 30-min entity-enrichment sweep that backfills `memory_entities` over existing memories via the configured LLM. Disabling it stops the automatic LLM spend/compute on a large corpus; the graph stream then only sees links created at write time. Parsed by `wenlan_core::db::entity_sweep_enabled()`, checked in `scheduler.rs` before the sweep fire-condition.
- `WENLAN_ENABLE_DOC_RECONCILE` — DEFAULT ON (opt out with `0`/`false`/`no`/`off`). Gates the background 30-min doc-reconcile sweep (doc-grounded revisions, L3): detects direct factual contradictions between ingested documents (`source_agent='folder'`) and agent captures, and stages human-gated rewrite+cite revisions on the existing pending-revisions queue (`/curate revisions`, `accept_revision`/`dismiss_revision`). Never mutates doc rows or captures; the human accept does. Bounded per tick: 50 rows/frontier, ≤25 LLM judge calls, vector top-k=5 with cosine ≥0.70, and new ticks hold while >20 doc-grounded revisions await review (checked once at tick start, so one in-flight tick can briefly overshoot the cap). Proposals whose rewrite is identical to the capture's current text are dropped as no-ops. Watermarks persist in `app_metadata` (`reconcile_frontier_docs` / `reconcile_frontier_captures`); a poison item is ejected with a `warn!` after 3 consecutive failed ticks. Parsed by `wenlan_core::db::doc_reconcile_enabled()`, orchestrated by `wenlan_core::reconcile::run_reconcile_tick`, fired from `scheduler.rs`.
- `WENLAN_ENABLE_CITATION_BACKFILL` — DEFAULT ON (opt out with `0`/`false`/`no`/`off`). Gates the background 30-min citation-backfill sweep: annotates legacy pages (`citations IS NULL`) with per-claim `[N]` markers against their memory-kind `page_evidence`, verified by the shared `crate::faithfulness` scorer (union-of-cited-sources ≥ 0.5). ANNOTATE-ONLY by construction: a deterministic guard strips markers from the model output and requires byte-equality (whitespace-normalized) with the existing body — any prose change is discarded; 3 rejected attempts poison-pill the page to `citations='[]'` (changelog notes it). ≤5 pages/tick, ≤5 LLM calls/tick, label `citation_annotate`. Parsed by `wenlan_core::db::citation_backfill_enabled()`, orchestrated by `wenlan_core::citations::run_citation_backfill_tick`, fired from `scheduler.rs`.
- - `WENLAN_ENABLE_EDGES_RECONCILE` — DEFAULT OFF (opt in with `1`/`true`/`yes`). Gates the background 30-min edges-parity reconcile sweep (M2 stage-d, spec v3 §7): re-derives the `edge_id` set the five legacy stores (`relations`, `page_sources`, `page_evidence`, `pages.citations`, `page_links`) imply, diffs it against the live active `edges` — matching stored structural columns (edge_type, src/dst kind+id), so an endpoint-corrupted row that kept its `edge_id` counts as drift — and stamps `edges_parity_watermark` with the drift count under the current dual-write epoch. READ-ONLY apart from that single watermark UPSERT; NO LLM. Never flips a reader: `reader_uses_edges` gates each cutover on a clean (drift 0) + current watermark, and the actual flip stays the manual `set_reader_cutover` lever. When enabled it runs inline through the shared foreground/resource/cooldown scheduler lane, never as a detached task. It stays default-OFF until the full-store scan has measured RSS and foreground-request-latency ceilings; the manual reader cutover is already default-OFF. Parsed by `wenlan_core::db::edges_reconcile_enabled()`, orchestrated by `wenlan_core::db::MemoryDB::reconcile_edges_parity`, fired from `scheduler.rs`.
- - `WENLAN_ENABLE_ENTITY_PAGE_RECONCILE` — DEFAULT OFF (opt in with `1`/`true`/`yes`). Gates the background 30-min entity/page-parity reconcile sweep (M3 PR-2, stage a): scans every `entities` row for exactly one live `kind='entity'` shadow page (via `entity_page_map`) with matching name/entity_type/confidence/space (incl. the unfiled sentinel fold)/aliases, plus orphan `entity_page_map` rows and orphan shadow pages absent from the map, and stamps `entity_page_parity_watermark` with the drift count under the current `entity_page_migration_state` epoch. READ-ONLY apart from that single watermark UPSERT; NO LLM. Mirrors `WENLAN_ENABLE_EDGES_RECONCILE` exactly — same cadence, same full-pass scheduler pacing (interval-gated regardless of backlog, always consumes a thermal turn), same reasoning for staying default-OFF until the full-store scan has measured RSS and foreground-request-latency ceilings. The sweep holds the single DB connection's mutex for the full pass (measured 18.88s at 10k entities on M2 Pro), so every foreground DB request queues behind it while it runs; chunked/staged sweeping is the stated follow-up gating any default-ON flip. The flag gates ONLY this ambient reconcile sweep — the reader flip is a separate, already-wired manual lever: `set_entity_reader_cutover` (off by default, per-consumer — `"scoped_entities"` today), which `reader_uses_entity_pages` fail-closes on a clean, current parity watermark. Parsed by `wenlan_core::db::entity_page_reconcile_enabled()`, orchestrated by `wenlan_core::db::MemoryDB::reconcile_entity_page_parity`, fired from `scheduler.rs`.
- `WENLAN_ENABLE_EDGE_GROUNDING_PROMOTE` — DEFAULT OFF (opt in with `1`/`true`/`yes`). Gates the background 30-min edge-grounding promotion sweep (M3g stage B, spec `docs/plans/2026-07-25-m3g-promotion-mechanics.md`): promotes stored `grounded=0` `relates` edges to `grounded=1`, writing `root_id`. It is the ONLY writer of `grounded=1`; extraction proposes (`grounded=0`), this validator grounds. **Two-gate promotion, both mandatory.** (1) Deterministic span pre-filter: the edge's Stage-A captured `payload.span.quote` must be located verbatim in the REAL stored `memories.content` of the edge's `source_memory_id` (never trust the stored char offsets — the quote is re-found in live content). A backlog edge with no captured span skips straight to gate 2. (2) A MANDATORY independent LLM entailment check — an independent judgment that the source text entails the structured triple, NOT the extracting model grading itself; span-presence alone can NEVER promote (it closes hallucinated-quote but not present-but-non-entailing / injected-text vectors). The source text is fenced as untrusted data in the prompt (`build_entailment_prompt`), so an instruction embedded in a document is judged, never obeyed. Only external-origin edges are eligible — the predicate is the daemon-recorded `memories.origin_class = 'document_ingest'` (`crates/wenlan-core/src/origin.rs`, migration 112), NOT a `source_agent` string match; agent-authored relations are `generated` and left `grounded=0`. The classification is stamped by the `RawDocument` ingest path (`upsert_documents`) from the writing path's own `source_agent` — that is where every external document and every agent capture enters — while the other writers that INSERT into `memories` (chat-export import, episode backfill, in-place rewrite) set the column explicitly at their own INSERT; `/api/memory/store` normalizes away any reserved value so no wire request can select an origin. Migration 112's one-time backfill grants `document_ingest` to `source_agent='folder'` rows only: the set is frozen historical fact rather than the live list, and pre-112 `obsidian` rows are left `generated` because some are Wenlan's own projected pages re-ingested and nothing in the row can tell them apart (a re-sync reclassifies them correctly). A new document-ingest connector adds its `source_agent` to `origin::DOCUMENT_INGEST_SOURCE_AGENTS` — one edit that both makes its documents groundable and bars a client from claiming the string. Batch-era relations whose quote no longer locates fail gate 1 and stay `grounded=0` (safe coverage loss, not a false ground). **Provider-gated** (unlike the two READ-ONLY reconcile sweeps): the entailment call needs the pinned LLM, so the lane's availability is `provider_available && edge_grounding_promote_enabled()`, mirroring reconcile / citation. Drains as a backlog slice — one entailment call per ambient turn (`AmbientBudgetProvider` caps one LLM call/turn), so the lane stays due while it progresses and backs off 30 min only when the backlog is empty. Bounded: ≤50 rows scanned per tick, ≤25 entailment calls per full `run_edge_grounding_tick` (the slice spends 1); a durable rowid cursor + poison-ejection after 3 consecutive failed ticks persist in `app_metadata` (`edge_grounding_cursor`). Each survivor mints a content-addressed provenance root via `acquire_provenance_root` (`root_kind='document_ingest'`, `source_identity` = the source memory's url-or-source_id) then monotone-flips `grounded 0→1` under an `AND grounded=0` idempotence guard — the flip is parity-invisible (structural edge columns unchanged; §1). No SQLite transaction spans the entailment call (§6.3): mint and flip are separate short transactions taken AFTER the LLM returns. Disabling the flag leaves already-promoted bits in place (monotone derived state). Parsed by `wenlan_core::db::edge_grounding_promote_enabled()`, orchestrated by `wenlan_core::edge_grounding::run_edge_grounding_slice` (ambient scheduler) / `run_edge_grounding_tick` (Gate 3 + hermetic tests), fired from `scheduler.rs`. Stays default-OFF until the false-grounding and foreground-latency gates (`docs/plans/2026-07-25-m3g-gate-criteria.md`) are measured on a real corpus (Stage C).
- `WENLAN_ENABLE_COMMUNITY_LEIDEN` — DEFAULT OFF (opt in with `1`/`true`/`yes`). Gates M4 PR-1's write-only persisted-community shadow. When enabled, the existing `Phase::CommunityDetection` slot processes at most one dirty space per firing through the durable lease, pure Leiden/incremental compute, and generation-CAS finalize path; it creates no new ambient scheduler lane. The legacy label-propagation producer still runs in the same phase and continues writing only `entities.community_id`, preserving the rollback surface while the M4 job writes only `communities`, `community_members`, `space_graph_state`, and `grouping_leases`. Grounded promotion, grounded retraction/reactivation, and entity merge mark a space dirty in their own transaction. Disabling the flag leaves recomputable shadow rows in place and restores the exact legacy-only phase behavior. Parsed by `wenlan_core::db::community_leiden_enabled()`, orchestrated by `MemoryDB::run_next_community_grouping_cycle`, fired from `refinery::Phase::CommunityDetection`.
- - `WENLAN_ENABLE_GENESIS_SHADOW` — DEFAULT OFF (opt in with `1`/`true`/`yes`/`on`). Gates the M6 PR-B genesis shadow lane (spec `docs/plans/2026-08-03-m6-pr-b-genesis-shadow-spec.md` §4.1). When enabled, `wenlan-server` spawns one sibling task that runs a bounded turn: the S0-5 startup recovery scan once, then **at most one unit of work per turn** against **one space**, chosen round-robin — one cursor-resumed frontier reconciliation slice (≤512 rows), one candidate prepare (≤16 proposals considered, ≤64 roots each; the shipped `genesis` lease makes the effective bound one prepare), or one dry-run finalization. Writes are confined to `genesis_*` tables, the shared `grouping_leases` registry (M6 phases only, via `m6::leases`), and M6's own `app_metadata` cursor key; the lane publishes nothing, writes no `genesis_enabled`, and touches no `pages`, `page_*`, `entities`, `relations`, `observations`, `edges`, `memories`, `chunks`, or `page_projection_outbox` row. NO LLM anywhere on the path — dry-run finalization verifies the eight CAS gates and stops (§2.4), and the lane takes no `ServerState` snapshot, so there is no provider handle to reach. Gated **at the `tokio::spawn`**, not inside the loop, so an OFF daemon has no task at all. **Stays default-OFF because the lane is unmeasured, not because it is unsafe:** a turn takes the single `MemoryDB` connection mutex every 1s idle / 100ms working, and neither its RSS nor its foreground-request-latency ceiling has been measured on a real corpus — the same reason `WENLAN_ENABLE_EDGES_RECONCILE` and `WENLAN_ENABLE_ENTITY_PAGE_RECONCILE` default OFF at a 30-minute cadence. Flipping it default-ON needs those two measurements on a representative store, plus the frozen S0-97 corpus the §7.3 benchmark is specified against (that generator does not exist yet). It is also what makes spec §10.3's rollback — "a flag flip plus a lease sweep" — an operation that exists. Parsed by `wenlan_core::db::genesis_shadow_enabled()`, driven by `wenlan_core::m6::shadow::run_genesis_shadow_turn`, fired from `register_optional_runtime_workers` in `crates/wenlan-server/src/main/runtime.rs`.
+ - `WENLAN_ENABLE_GENESIS_SHADOW` — DEFAULT OFF (opt in with `1`/`true`/`yes`/`on`). Gates the M6 PR-B genesis shadow lane (spec `docs/plans/2026-08-03-m6-pr-b-genesis-shadow-spec.md` §4.1). When enabled, `wenlan-server` spawns one sibling task that runs a bounded turn: the S0-5 startup recovery scan once, then **at most one unit of work per turn** against **one space**, chosen round-robin — one cursor-resumed frontier reconciliation slice (≤512 rows), one candidate prepare (≤16 proposals considered, ≤64 roots each; the shipped `genesis` lease makes the effective bound one prepare), or one dry-run finalization. Writes are confined to `genesis_*` tables, the shared `grouping_leases` registry (M6 phases only, via `m6::leases`), and M6's own `app_metadata` cursor key; the lane publishes nothing, writes no `genesis_enabled`, and touches no `pages`, `page_*`, `entities`, `relations`, `observations`, `edges`, `memories`, `chunks`, or `page_projection_outbox` row. NO LLM anywhere on the path — dry-run finalization verifies the eight CAS gates and stops (§2.4), and the lane takes no `ServerState` snapshot, so there is no provider handle to reach. Gated **at the `tokio::spawn`**, not inside the loop, so an OFF daemon has no task at all. **Stays default-OFF because the lane is unmeasured, not because it is unsafe:** a turn takes the single `MemoryDB` connection mutex every 1s idle / 100ms working, and neither its RSS nor its foreground-request-latency ceiling has been measured on a real corpus — the same reason the now-retired `WENLAN_ENABLE_EDGES_RECONCILE` and `WENLAN_ENABLE_ENTITY_PAGE_RECONCILE` sweeps (G6 Stage 2 PR 2a) defaulted OFF at a 30-minute cadence. Flipping it default-ON needs those two measurements on a representative store, plus the frozen S0-97 corpus the §7.3 benchmark is specified against (that generator does not exist yet). It is also what makes spec §10.3's rollback — "a flag flip plus a lease sweep" — an operation that exists. Parsed by `wenlan_core::db::genesis_shadow_enabled()`, driven by `wenlan_core::m6::shadow::run_genesis_shadow_turn`, fired from `register_optional_runtime_workers` in `crates/wenlan-server/src/main/runtime.rs`.
- `WENLAN_PREFER_ON_DEVICE_COMPILE` — opt-in (default OFF). Compile routing for the Emergence phase follows spec §3.1: (1) a cloud/API provider compiles in the daemon with the LLM coherence gate; (2) otherwise eligible clusters are left pending for the existing agent lane (`POST /api/distill` pending clusters + `/distill` skill); (3) only when this flag is truthy may a healthy on-device provider compile those new clusters directly. The on-device path skips the LLM coherence gate; the page keep-card carries that judgment. An unavailable on-device provider still leaves clusters pending. Parsed by `on_device_compile_preferred()` in `crates/wenlan-core/src/refinery/mod.rs`, applied only at the Emergence phase's compile-routing decision inside `run_periodic_steep_with_api` (never inside `distill_pages_scoped`/`distill_pages_scoped_gated` themselves, so every other caller of those functions is unaffected). Does NOT gate the separate ReDistill-phase page refresh or the reserved Overview-page refresh (`maybe_refresh_overview_page`) — those already-existing pages keep refreshing via `compile_llm` regardless, since they are refresh ops on established rows, not the "which lane grows a brand-new page" decision this flag targets.
### Consolidation demotion (P3, always-on, no flag)
`compute_distill_demotion` (db.rs) multiplies a distilled memory's FINAL cross-rerank score by 0.7 at distill time, recovering linearly to 1.0 over 5 days (`memories.last_distilled_at` stamp, set by both distill paths + the scoped-match attach). Applied POST-CE in `search_memory_cross_rerank_cued` so the cross-encoder score overwrite cannot erase it (adversarial finding C1); the quick path (`search_memory`) and `/api/context` surface are untouched by design. Eval verdict (2026-06-10, SHIPPED): paired A/B, N=3/arm, both arms same binary on freshly re-seeded cached scenario DBs (`seed_scenario_dbs_complete` with the P3 binary; stamps live: LoCoMo 206 source_ids/18 new pages, LME 45/2; all runs within 7h of seeding, well inside the 5-day window), BGE-Base-EN-v1.5-Q + CPU bge-reranker-v2-m3, page-channel ON. LoCoMo (byte-deterministic across runs): NDCG@10 −0.07pt, recall@5 +0.12pt, coverage unchanged. LME: NDCG@10 +0.29pt (ON 0.5979±0.0045 vs OFF 0.5950±0.0027), MRR +0.35pt, cov_blind −0.12pt (inside one OFF-stddev). All plan gate criteria PASS (coverage non-regressing within noise, NDCG within 1pt, `distilled_memory_demoted_but_reachable` green). Chat-context N=3 leg was SKIPPED as a structural null by construction: the `/api/context` eval path calls `search_memory` + `search_pages`, never the cross-rerank function where the demotion applies, so both arms are byte-identical there — running it would only launder a no-op as a "pass". Raw per-run baselines: gitignored `app/eval/baselines/{locomo,longmemeval}_p3ab_demotion_{ON,OFF}_run{1..3}.json`.