Gate finding 8: the canary's trivial-task check ran on an EMPTY store — the capsule-level zero-memory proof was vacuous. It now seeds a high-importance fact distractor plus a personality row before assembling; the trivial task must still produce zero memory blocks and zero memory tokens. Also lands the discharge record: docs updated (floor-in-every-mode claim, known ranking bias, typed persona lane, write-time memory_type, migration-time backfill) and the plan's Completion Record gains the Discharge Record section mapping every gate finding to its commit and red-first evidence.
6.9 KiB
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_operatorneurons contributor) is REMOVED: importance alone never injects. - The
gate_consolidatedflat-0.8 shortcut insidecached_recallis REMOVED — and the now-deadgate_consolidatedfunction itself is deleted frommemory_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.pystays dormant-until-dependency (its ID-path defect is #27 territory — not fixed here).auto_context.pyis 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.