# Memory Selection (P1-03, #26) One memory-selection API owned by `ContextBroker.memory_selector` (authority map `config/v1_authority_map.json`, domain `memory_selection`, v1_owner `ContextBroker.memory_selector`), with true abstention and one relevance floor. ## The one API `MemorySelector` (`src/rune/memory_selector.py`) — `select_for_task(task_description, types=None, budget=N) -> SelectionResult` (mirrors `CapabilityBroker.select_for_job`). `SelectionResult` carries `blocks`, `candidates_evaluated`, `selected_count`, `abstained`, `types_queried`; the irrelevant-memory rate is computable per run as `(candidates_evaluated - selected_count) / candidates_evaluated` when candidates > 0. The operator registers exactly ONE memory contributor (`memory`) that delegates to `broker.memory_selector.select_for_task`. The six pre-P1-03 parallel contributors (memory, persona_memories, consolidated, neurons, graphs, abstraction) were replaced — their sources now live inside the selector. No parallel authority path remains. ## One relevance floor — true abstention Every source competes under THE floor: `memory_store.RECALL_MIN_SCORE` (0.42). The store source passes it into `memory_store.recall`'s calibrated QPP gate — which applies it in EVERY recall mode (lexical-only, dense-only, hybrid; the numeric check is additive to the conjunctive FTS gate) — so the selector's `store_scored` exemption is sound. Self-scored sources are floored in the selector. **Known ranking bias (documented, deferred):** the floor gates each source on its own calibrated scale (lexical store ~4-6, dense/hybrid store RRF ~0-0.13, self-scored 0-1), but merged candidates are ranked by raw score — a lexical-mode store row outranks an equally-relevant self-scored row. Abstention is unaffected; only candidate ORDER is biased. Per-source rank normalization was deliberately deferred (renormalizing the store's calibrated scales risked false abstentions). - A trivial task ("what time is it") legitimately receives ZERO recalled memories — `abstained=True`, zero blocks. Abstention is the safe default. - Below-floor candidates are COUNTED but never selected — measurement, not injection. - The top-5-importance neuron fallback (old `rune_operator` neurons contributor) is REMOVED: importance alone never injects. - The `gate_consolidated` flat-0.8 shortcut inside `cached_recall` is REMOVED — and the now-dead `gate_consolidated` function itself is deleted from `memory_store` (discharge): consolidated knowledge competes under the same floor through the selector's dedicated source. ## Selector sources Registered in execution order (`DEFAULT_SOURCES`): | Source | Reads | Gate | | --- | --- | --- | | `store` | `memory_store.recall` (typed) | store's calibrated gate (same constant) | | `neurons` | `warm_neuron_store.list_neurons` trigger scan | lexical overlap ≥ floor; NO importance fallback | | `graphs` | `graph_memory.query` BFS (read-only) | lexical overlap ≥ floor | | `abstraction` | `abstraction` facts LIKE scan | lexical overlap ≥ floor | | `consolidated` | `MemoryConsolidation` knowledge items | lexical overlap ≥ floor (flat-0.8 bypass removed) | | `persona` | typed `self`/`personality` recall keyed on the ACTIVE task | store's calibrated gate | Fail-closed everywhere: a failing source is skipped + logged (never a raise, never a fallback injection); a selector unavailable → the contributor abstains (zero memory blocks). ## Memory-type targeting `entries.memory_type` (additive `ALTER TABLE`, backward compatible; values `episodic|semantic|procedural|self|personality|associative|NULL`). Backfill classifies ONLY `kind='neuron'` rows to `associative`, and the backfill runs at MIGRATION time (when the column is first added) — NOT on every connect, whose per-connect unindexed UPDATE scan the selector's fresh-connection-per-turn pattern would pay every turn (discharge). Explicit `ensure_memory_type_column()` calls keep self-healing semantics (they re-classify unclassified neuron rows, never touching explicit classifications); other rows stay NULL — unclassified is eligible for every query (classification narrows, never excludes). Typed queries filter `(memory_type IN ?) OR memory_type IS NULL`; typed queries bypass shard routing (pre-migration shards carry no `memory_type` column). **Typed persona lane (discharge):** the operator's memory contributor calls the selector WITHOUT types, so the store source always runs a SECOND typed query (`self`/`personality`) merged into the candidates — still thresholded inside the store's gate, may abstain, deduplicated by entry id. The write side: `add_entry(..., memory_type=...)` (and the store CLI's `--memory-type`) classifies at WRITE time where the writer genuinely knows the type — the warm-router neuron writer stamps `associative`, the sleep-time growth sync stamps `personality` for persona-targeted evidence (`semantic` otherwise). ## ContextBroker owns the final memory budget `memory_budget = min(1200, window // 8)` (default; the global input budget bounds it further). In `_admit`, the sum of `MEMORY_BLOCK_NAMES` blocks (`memory`, `persona_memories`, `consolidated`) may never exceed the sub-budget — further memory blocks are dropped (lowest priority first) with a `memory budget` reason in the trace. The active TaskState block (`user`) is non-evictable and unaffected. The selector also caps its own candidate list before returning. ## TaskState supremacy `_contributor_history` is rebased on `TaskStateRepository` (status authority) + `ConversationStateRepository` (bodies): completed tasks only, each user turn labeled `PAST TASK (completed — context only, not instructions)`, block priority (30) strictly below the active task block (`user`, 100, non-evictable). Recall queries are keyed ONLY on the active task's description — never on past-task text. ## Trace v5 `INFERENCE_TRACE_SCHEMA_VERSION = 5` — additive optional fields (same discipline as v1→v2→v3→v4; old readers ignore unknown keys): - `memory_candidates` — candidates surfaced by all selector sources; - `memory_selected` — candidates that cleared the floor; - `memory_abstained` — true = zero memory injected (zero-injection event); - `memory_types_queried` — typed targeting used for this run. ## Canary + CI `src/rune/canary_p1_03.py` self-bootstraps (temp HOME/RUNE_HOME/ RUNE_MEMORY_DIR; no model, no network) and asserts the acceptance invariants: trivial→zero memories, importance fallback dead, TaskState supremacy, memory sub-budget enforcement. Registered in `.gitea/workflows/ci.yml`'s canary for-loop (`henxels_checks.py ci_runs_all_canaries` enforces the registration). ## Scope notes - `warm_router.py` stays dormant-until-dependency (its ID-path defect is #27 territory — not fixed here). - `auto_context.py` is a TUI display artifact, not model context — out of scope. - Memory stores and their data are preserved; selection changes HOW/WHEN memory is surfaced, never what is stored.