Skip to content

Memory

memlink-hybrid is the durable memory service. The root README describes it as a standalone service backed by PostgreSQL schemas including core, world, and moth. The public API contract is memlink-hybrid/openapi.yaml.

The repo guide routes memory-service work through the memlink README, OpenAPI contract, implementation plan, ADRs, domain docs, operations docs, and then the domain, application, adapters, and infrastructure code layers.

Native /v1/events writes an append-only raw event through IngestEventUseCase. For user-authored turns it also persists fast-lane deterministic fact/entity records immediately via derive_fast_lane_records_from_event; this path does not create an asc_jobs derivation row. The compatibility batch ingest path can call DispatchDerivationUseCase, which creates the derivation job, derives an episode plus facts/entities, merges optional supplemental LLM candidates, and enqueues embedding jobs when enabled.

Deterministic extraction is intentionally conservative and source-grounded. As of 2026-06-12, employer values are bounded at sentence/conjunction boundaries and “I work as a …” statements produce a role fact. The profile extractor also handles company-name and called-work-site phrasing, so a user statement such as “My company name is Sodexo and I work in a mining village called Pannawonica” stores structured employer=Sodexo and work_site=Pannawonica facts instead of relying on the raw episode text. Async derivation now passes the same relation-extraction flag into derive_records_from_event as the inline dispatch path and persists generated relation rows when the flag is enabled.

Memlink’s non-embedding LLM service config normalizes clients to the Codex login path: provider openai-codex, model gpt-5.5, and base URL https://chatgpt.com/backend-api. The provider and model are pinned constants in config.py (_llm_provider() / _llm_model()) — they are NOT read from the environment, so any stale non-Codex provider/model env value is ignored for this path. The base URL is overridable via MEMLINK_HYBRID_LLM_BASE_URL. Credentials are read from the same Codex auth file shape used by runtime chat/planner, including tokens, flat auth JSON, and profile auth JSON variants.

Embeddings are the exception: compose enables OpenAI API embeddings with MEMLINK_HYBRID_EMBEDDING_PROVIDER=openai, MEMLINK_HYBRID_EMBEDDING_MODEL=text-embedding-3-large, and MEMLINK_HYBRID_EMBEDDING_DIM=3072; provider keys are brokered, not accepted as raw environment secrets. The Agent Vault session used for that brokering is re-minted by the container entrypoint every ~6h and written to the file named by MEMLINK_HYBRID_SECRET_BROKER_SESSION_TOKEN_FILE (a path, not a secret); the service re-reads it per fetch so embeddings and scope-auth survive the 24h session TTL instead of going dark.

Compose also enables the relation and structured retrieval gates for the deployed Memlink service: MEMLINK_HYBRID_RELATION_EXTRACTION_ENABLED=1, MEMLINK_HYBRID_LLM_STRUCTURED_SCHEMA_ENABLED=1, MEMLINK_HYBRID_RETRIEVAL_ORCHESTRATOR_ENABLED=1, and MEMLINK_HYBRID_RETRIEVAL_ROUTER_ENABLED=1. Relation extraction has two producers (application/dispatch_derivation.py): a deterministic high-precision regex floor (application/relation_extractors.py, ~0 on ordinary chat by design) and the LLM supplemental “recall” extractor — the latter only emits relations when MEMLINK_HYBRID_LLM_STRUCTURED_SCHEMA_ENABLED is on (else its prompt is facts/entities only, so world.relations stays 0). With the schema flag on the LLM extractor runs in structured-schema mode and returns grounded relations (sanitized + resolved + owner-scoped); schema mode also applies JSON-schema enforcement to facts/entities. The retrieval router classifies memory queries into route-specific retrieval strategies with a general-route fallback.

MEMLINK_HYBRID_LLM_REASONING_EFFORT sets the Codex reasoning.effort on BOTH the supplemental extractor (llm_supplemental_extractor.py) and the Luna consolidation client (luna_consolidation_llm_anthropic.py — despite the legacy _anthropic suffix, that client calls Codex /responses, not Anthropic). Valid: none|minimal|low|medium|high|xhigh via the shared infrastructure/codex_reasoning.py helper; unset/invalid omits the field (Codex API default). Deployed xhigh. (The Morpheus drain runs its own xhigh via MOTH_HYBRID_MORPHEUS_ENDLESS_DREAMING_CODEX_REASONING_EFFORT.)

Broad self-profile retrieval queries such as “tell me everything you know about me” are classified as profile-summary retrieval and prefer structured facts or relations over raw episode text when those records exist.

memlink-hybrid/src/memlink_hybrid/application/eval/ is an offline, owner-scoped accuracy harness — the backbone the PR4–PR10 memory program gates against. It is behavior-preserving: it changes no live answer and no flag flips a reader. The pure runner (harness.py, mirroring the planner-eval pattern) takes an injected retrieval/answer callable and a JudgePort and imports no LLM client, socket, or secret; it reports metrics per ability slice (information_extraction, multi_session, temporal_reasoning, knowledge_update, abstention) with a confidence interval beside every number and p50/p95 latency beside accuracy — never a single aggregate. Beyond recall/MRR/nDCG/answer-correctness/faithfulness/abstention, the harness also emits the SAGE temporal/multi-hop metrics (knowledge_update_correctness, stale_answer_rate, as_of_temporal_resolution, multihop_trace_recall_at_depth) — but only for cases carrying the matching optional EvalCase annotation (stale_answer / as_of_query / required_hops), so an un-annotated corpus’s output is byte-identical (no new keys). Answer-correctness is scored through a JudgePort (application/ports/eval_judge.py): a pure DeterministicJudge for offline/CI slices, and a Codex-backed judge (infrastructure/codex_eval_judge.py) that reuses the same mounted MEMLINK_HYBRID_LLM_CODEX_AUTH_FILE seam as the extractor (not a broker alias) and fails closed on a malformed reply. Judge trust is itself gated by a calibration step (judge↔human agreement floor + adversarial false-accept rejection), and the publishable scorecard.py carries only numbers, slice labels, and pinned model ids — never raw question/answer text (INV-006).

The golden corpus has two homes: a byte-stable in-repo tests/eval/corpus.jsonl for deterministic slices, and the DB-backed core.eval_goldens table (migration 0082_eval_goldens) for owner-scoped cases. core.eval_goldens is FORCE-RLS isolated by app.owner_id through the 0079 steelmoth.world_scope_matches(owner_id) helper (strict owner-only — no worker-scope escape), and the loader (adapters/outbound/eval_goldens_repository.py) sets the owner GUC and keeps an explicit WHERE owner_id backstop, so the harness runs through the same owner-isolation path it measures.

Shadow Retrieval Telemetry (PR6, measurement-only)

Section titled “Shadow Retrieval Telemetry (PR6, measurement-only)”

The FTS/pgvector candidate readers in consolidations_repository.search_support run in shadow when MEMLINK_HYBRID_FTS_SHADOW / MEMLINK_HYBRID_PGVECTOR_SHADOW are set: they generate owner-scoped candidates and emit telemetry but never merge (the if mode == "on" branches stay unreached, so the bundle is byte-identical to flags-off). PR6 enriches the retrieval.candidates events with hit_rate, reader_latency_ms, error, and scope_owner_present (RLS-confound guard: a count == 0 with scope_owner_present=False means an empty owner scope, not “found nothing”), plus pgvector requested_count, confidence_band (CRAG, from cosine distance), iterative_scan_mode, and ef_search. The vector fetch applies hnsw.iterative_scan (relaxed_order, PR7’s ship config) + hnsw.ef_search (100) via the SAVEPOINT-guarded PostgresDatabase.fetch_all_with_local_settings, preserving the owner GUC so 0079 FORCE RLS still binds. A retrieval.shadow_rrf event previews PR7 fusion via ranker.fuse_rrf (k=60) — log-only. Measurement-only (no answer change, no migration); events log ids/counts/ranks/latency/distances only, never raw query or value text (RFC 0024 §5.1). PR7 reads the soak telemetry.

Live RRF Hybrid Selection (PR7, default-off)

Section titled “Live RRF Hybrid Selection (PR7, default-off)”

search_support is candidate generation into the discovery cut (it returns ranked[:_DISCOVERY_LIMIT], 20); the bundle use-case (RetrieveSupportBundleUseCase) re-hydrates and re-ranks those survivors for the served order, where PR5’s structured-beats-raw guardrail lives (MEMLINK_HYBRID_RANKING_GUARDRAILS_ENABLED). PR7 makes the readers’ live ("on") path safe to enable: when MEMLINK_HYBRID_RRF_FUSION_ENABLED is on and a reader is live (and relation expansion is off — PR8 owns its rank list), _rrf_select picks which candidates survive the cut by Reciprocal Rank Fusion (ranker.fuse_rrf, k=60) of the legacy/FTS/pgvector rank lists, replacing the old max(base,0.05)/flat-0.15 floors, and writes the normalized fused score onto each evidence so the use-case reads a coherent discovered_score. RRF is the primary selection signal; structured_trust_grade is only a tie-break for TRUST_GRADE_QUERY_CLASSES (canonical home: retrieval_policy), so selection never starves recall. The pgvector HNSW overfiltering guard (hnsw.iterative_scan/ef_search) now applies on the "on" path too, and a CRAG retrieval.degrade_to_fts event fires when the live vector arm errors or returns zero rows. Off → the returned bundle is byte-identical. Enable is a separate #scope:compose + HR6 step that flips FTS_ENABLED + PGVECTOR_ENABLED + RRF_FUSION_ENABLED + RANKING_GUARDRAILS_ENABLED.

Trust-grade tie-break on relational recall

Section titled “Trust-grade tie-break on relational recall”

When comprehensive recall fuses its rank lists, ties on the fused score are broken by a trust ladder: a current fact beats a current relationship, which beats a session episode, which beats a raw chat snippet (see structured_trust_grade in retrieval_policy.py). As of ADR 0169 this tie-break fires not only for the tuned trust query classes but also whenever the gathered evidence contains at least one CURRENT relationship edge — i.e. the “is X related to me” relational case, which classifies as general and previously skipped the tie-break. Effect: a current relationship edge can no longer be out-ranked by an episodic echo of the owner’s own past question. It is a strict no-op for queries that resolve no current relationship edge, so non-relational recall is unchanged. Cite: comprehensive_recall.py _fuse_and_rank + _bundle_has_current_relation; retrieval_policy.py TRUST_GRADE_QUERY_CLASSES / structured_trust_grade.

Named-seed edge promotion + echo suppression (ADR 0171)

Section titled “Named-seed edge promotion + echo suppression (ADR 0171)”

When a question names an entity (e.g. “is pannawonica related to me?”), the gather now (a) promotes the current edges that touch that named entity into a dedicated front-priority RRF rank list (seed_edges), bounded to _MAX_NAMED_SEED_EDGES and excluding the owner-self hub, so the literal subject edge (works_at -> Pannawonica) survives to the served top-N instead of being truncated; and (b) drops the owner’s OWN verbatim question-echoes (episodes whose normalized text equals the query) from the discovery rank list before fusion, so a backlog of identical past questions no longer crowds out real facts/edges. Both changes live entirely inside comprehensive_recall.py — no new ranker/store/worker (INV-025/027). The owner-self anchor is excluded from named-seed promotion by both the synthetic owner id and entity_type=='owner', because the live owner-self is a materialized owner-typed entity whose id is not the synthetic id. Cited source: comprehensive_recall.py _named_seed_ids / _named_seed_edge_ids / _discovery_ids_without_query_echoes. Known residual: list_current_entities_for_owner has no deterministic ORDER BY, so which of several same-named entities seeds is non-deterministic under _MAX_SEEDS; the subject edge still survives via the owner-self anchor. Deploy = stm-memory-memlink rebuild+recreate (HR6); inert unless comprehensive_recall_enabled is ON in the live container.

Currency-grade hedging (Phase 0 / Option A, ADR 0170)

Section titled “Currency-grade hedging (Phase 0 / Option A, ADR 0170)”

A fact or relationship carries a currency-grade: grounded (confirmed) or provisional (told-but-not-yet-confirmed). The grade lives on the underlying row — world.relations.confidence_state for an edge (RelationObjectRecord.confidence_state) and a fact’s attributes['confidence_state'] — and is now threaded through the two answer-context carriers (SupportEvidence, CompatRetrieveObject, both via an optional confidence_state field defaulting to grounded) into build_answer_context_v2.

At render, a line is marked with the trailing suffix " (user-stated, unconfirmed)" IFF it is CURRENT (does not start with _PAST_PREFIX, “Previously (no longer current): “) AND its grade is provisional. This applies to both the discrete [F{n}] fact record and the relation/entity bullet, in both the grouped (live) and flat render branches. A grounded current line is a plain assertion; a past/superseded line keeps its _PAST_PREFIX and is never marked-as-current. (2026-08-18, ADR 0247: the original second-person prefix "Based on what you have told me, " was re-voiced to the suffix — the prefix is instruction-shaped prose that the prompt-injection scanner scored high, tipping whole memory bundles into whole-block drops on ~15% of turns.)

The brain is told (one appended sentence in the answer-context guidance) to keep the marker attached when the item is used, so a provisional fact is conveyed as told-not-confirmed, never asserted hard. The marker is plain prose in parentheses: it does not consume a [F{n}] index and does not trip the [[CONFLICT/NOT_KNOWN]] marker scan.

Sizing note: the answer-context header reserve (reserve_for_header) is 560 — it must exceed the longest header+guidance block (person_detail, 475 chars after the 2026-08-18 re-voice) or build_answer_context_v2 trips its budget_below_header fallback and drops every item.

Scan enforcement granularity (2026-08-18, ADR 0247): memory evidence entering prompts is injection-scanned on the read path (INV-300); under enforce, a flagged block is now rescued line-by-line (only flagged lines drop; a flagged block with no individually-flagged line still drops whole). The renderer’s wording is therefore a scanner-compatibility surface: second-person instruction-shaped phrasing in retrieved bundles must be avoided and re-measured when changed.

The behavior/rollout flags config.py and the retrieval adapter (adapters/outbound/consolidations_repository.py) read from the environment are listed below with their CURRENT operator-deployment state (compose.steelmoth.private.yml). All are default-off (or default-on where noted) in code; “on” means enabled on the operator deployment. memlink-hybrid/tests/test_flag_inventory.py asserts every behavior flag in those two files is named here (and that this page names no flag the runtime no longer reads) — an INV-015-style drift guard. Pure plumbing keys (DB connection/pool, embedding/LLM provider identifiers, scope-auth signing-alias + JWKS transport, worker/pgvector tuning) live in that test’s wiki operational-config allowlist rather than here. The full per-flag default + purpose table is in memlink-hybrid/README.md.

  • MEMLINK_HYBRID_DERIVATION_ASYNC_ENABLED (default-on) — async derivation dispatch off the ingest path.
  • MEMLINK_HYBRID_DERIVATION_WORKER_ENABLED (default-on, on) — background derivation worker loop.
  • MEMLINK_HYBRID_LLM_EXTRACTION_ENABLED (on) — LLM supplemental fact/entity extraction.
  • MEMLINK_HYBRID_LLM_STRUCTURED_SCHEMA_ENABLED (on) — structured-schema LLM extraction; required for the supplemental extractor to emit world.relations.
  • MEMLINK_HYBRID_RELATION_EXTRACTION_ENABLED (on) — relation extraction during derivation (deterministic floor + LLM recall).
  • MEMLINK_HYBRID_LLM_RELATION_WRITER_ENABLED (default-off; on for the operator) — relations-first LLM extractor prompt (RFC 0097): makes relations a primary extraction goal, lifting the emit rate that left world.relations near-empty.
  • MEMLINK_HYBRID_RECONCILE_DECISION_ENABLED (on) — emit reconcile_op decision attributes (supersede-not-delete bookkeeping).
  • MEMLINK_HYBRID_EMBEDDING_ENABLED (on) — embedding generation write path (provider key brokered).
  • MEMLINK_HYBRID_LLM_TIMEOUT_MS (code default 4500; operator-deployed 60000 = 60 s) — non-embedding LLM call timeout.
  • MEMLINK_HYBRID_LUNA_CONSOLIDATION_ENABLED (off; legacy fallback MEMLINK_HYBRID_MORPHEUS_LLM_CONSOLIDATION_ENABLED still honored) — LLM-backed Luna consolidation, per-day quota-gated.
  • MEMLINK_HYBRID_EMAIL_EXTRACTION_ENABLED (off; RFC 0098 Phase 3) — independent surgical gate for the deterministic email-contact extraction branch (mints Phase 3 objects/links without switching on the broad LLM pipeline).
  • MEMLINK_HYBRID_DETERMINISTIC_EXTRACTION_ENABLED (default-on; off for the operator, V4-Flash-primary extraction, task tp_5056) — when off, the deterministic/pattern fact+entity extractors are skipped entirely and the LLM (V4-Flash) supplemental extractor runs standalone as the sole fact source.
  • MEMLINK_HYBRID_EXTRACTION_FAILOVER_ENABLED (default-on; V4-Flash reliability, task tp_25dd) — when on, a V4-Flash outage queues events (no zero-fact persist) and opens a row in world.extraction_outage_log; after MEMLINK_HYBRID_EXTRACTION_FAILOVER_BRIDGE_THRESHOLD_SECONDS (default 5h, Mark’s directive) the deterministic extractor switches back on PER-CALL as a temporary bridge (facts marked ingest_profile='deterministic_bridge'); on recovery V4-Flash resumes from the per-owner world.extraction_watermark and force-re-extracts the outage gap so it replaces the bridge facts. The worker probes V4-Flash every MEMLINK_HYBRID_EXTRACTION_FAILOVER_PROBE_INTERVAL_SECONDS (default 60s) to detect recovery and defers a queued job for MEMLINK_HYBRID_EXTRACTION_FAILOVER_REQUEUE_DELAY_SECONDS (default 30s) so it does not spin on a down provider. Off ⇒ today’s behaviour (V4-Flash primary, no queue-on-failure). The outage state is surfaced on /readyz.
  • MEMLINK_HYBRID_WINDOW_EXTRACTION_ENABLED (off; V4-Flash Phase 3 Stage-2, shadow-first live in compose Phase F) — master gate for the windowed contextual extraction pass (DeepSeek V4 Pro over a bounded conversation window). Off ⇒ worker defers derive_window jobs and /v2/window-backfill/start 404s.
  • MEMLINK_HYBRID_WINDOW_EXTRACTION_SHADOW (on; default-on, shadow-first) — when on, the window pass runs and logs what it would persist but skips writes until shadow is flipped off.
  • MEMLINK_HYBRID_WINDOW_EXTRACTION_REALTIME_ENABLED (off; Phase D) — after compat ingest dispatches derive_event, also enqueue one derive_window job anchored at the new user message (requires master gate on).
  • MEMLINK_HYBRID_SELF_MEMORY_GRAPH_SHADOW (on; default-on, shadow-first, Phase 4 self_memory → graph pilot) — when on (and ENABLED is off), a confirmed entity-linked self-memory fact (employer/location) logs the works_at/lives_in graph edge and entity rows it would mint but skips every persist.
  • MEMLINK_HYBRID_SELF_MEMORY_GRAPH_ENABLED (off; default-off, Phase 4 self_memory → graph pilot) — when on, the curated-record write also persists the graph edge (+ owner self-entity, + resolved/minted target entity) into world.relations/world.entities, first-edge-wins idempotent.
  • MEMLINK_HYBRID_SELF_MEMORY_GRAPH_ENABLED_OWNERS (empty; operator-first rollout, 2026-08-05) — comma-separated owner ids that persist graph edges even while ENABLED is off; unlisted owners keep shadow (log-only) behaviour. Live in compose with the operator’s owner id.

Identity resolution, self-heal & anomaly detection (RFC 0098 / RFC 0100)

Section titled “Identity resolution, self-heal & anomaly detection (RFC 0098 / RFC 0100)”
  • MEMLINK_HYBRID_IDENTITY_LEDGER_ENABLED (off; RFC 0098 Phase 2) — master gate for accretive confidence-ledger identity resolution (off ⇒ records nothing; shadow-first — accumulate scores before any merge).
  • MEMLINK_HYBRID_IDENTITY_AUTO_MERGE_ENABLED (off; RFC 0098 Phase 2) — auto-merge sub-gate; an above-threshold pair auto-merges only with the master ledger gate also on (off ⇒ surfaced to the Review queue instead).
  • MEMLINK_HYBRID_SELF_HEAL_ENABLED (off; RFC 0100 Phase B) — master gate for the scheduled, INERT demote-not-delete self-heal pass (on ⇒ runs DRY unless the wet-run gate also permits; off ⇒ the driver never fans out to it).
  • MEMLINK_HYBRID_SELF_HEAL_WET_RUN_ENABLED (off; RFC 0100 Phase B) — separate wet-run gate for self-heal (two-key inert discipline: a write needs BOTH this and the master gate).
  • MEMLINK_HYBRID_ANOMALY_DETECTION_ENABLED (default-on; RFC 0100 Phase C) — read-only, log-only memory-quality anomaly detector (mutates nothing; ships live — the operator-gated part is only the future dashboard panel).
  • MEMLINK_HYBRID_RETRIEVAL_ORCHESTRATOR_ENABLED (on) — retrieval orchestrator path.
  • MEMLINK_HYBRID_RETRIEVAL_ROUTER_ENABLED (on) — query-class retrieval router with a general-route fallback.
  • MEMLINK_HYBRID_PROFILE_RETRIEVAL_ENABLED (on) — profile/self-dump route + multi-valued-slot facts.
  • MEMLINK_HYBRID_FACT_SIBLING_CONSULT_ENABLED (on; Stage 2, ADR 0168) — FactRoute sibling-slot consult: on a precise FactRoute MISS (count==0), the slot router consults the declared grounded sibling (work_siteemployer) before abstaining, so “where do I work?” surfaces the grounded employer fact (e.g. Sodexo) instead of not_known. Defaults ON (operator ship-live; pure-additive — only ever turns a primary miss into a grounded sibling answer or leaves the honest not_known untouched). Set to 0 for the primary-miss → not_known behaviour.
  • MEMLINK_HYBRID_FTS_ENABLED / MEMLINK_HYBRID_FTS_SHADOW (live / shadow) — FTS lexical candidate reader (live and telemetry-only modes).
  • MEMLINK_HYBRID_PGVECTOR_ENABLED / MEMLINK_HYBRID_PGVECTOR_SHADOW (live / shadow) — pgvector semantic candidate reader.
  • MEMLINK_HYBRID_RRF_FUSION_ENABLED (on) — Reciprocal Rank Fusion for discovery candidate selection.
  • MEMLINK_HYBRID_RANKING_GUARDRAILS_ENABLED (on) — structured-beats-raw served-order guardrail.
  • MEMLINK_HYBRID_RANKER_V2_SHADOW (shadow) — ranker v2 shadow scoring (telemetry only).
  • MEMLINK_HYBRID_RERANKER_ENABLED (off) — post-fusion semantic rerank stage (embedding-cosine; cross-encoder deferred).
  • MEMLINK_HYBRID_COMPREHENSIVE_RECALL_ENABLED (off; RFC 0102) — master gate for the future default-flip of the evidence-gated comprehensive recall gather (Phase 3). Unwired to dispatch in Phase 1.
  • MEMLINK_HYBRID_COMPREHENSIVE_RECALL_SHADOW (off; RFC 0102 Phase 1) — run the comprehensive recall gather in SHADOW alongside live recall and emit one PII-free recall.comprehensive_shadow compare event (counts + latency + bundle-size delta + relation edges the gather added); the live result is returned unchanged. Off → the gather is never invoked (byte-identical to today).
  • MEMLINK_HYBRID_TRANSIENT_TURN_EPISODE_DEMOTE_ENABLED (on) — browse/ tool-command noise filter (demote-not-delete; facts never demoted).
  • MEMLINK_HYBRID_RECALL_ECHO_HYGIENE_ENABLED (default-on) — recall echo hygiene (the 2026-07-05 “since 2004” incident, where recall served the owner’s own past test questions instead of the facts taught the same day). The PRIMARY echo signal is DATA-grounded, not text-shape: an episode/candidate whose source event derived no fact and no relation at ingest (list_zero_yield_object_ids, one provenance anti-join per request) carries no durable information whatever its phrasing or language — so the whole backlog is covered with no backfill write and no word-list to leak (live probes proved phrasing lists leak: “so …” fillers and no-question-mark questions both survived a regex-only cut). One gate, three defenses: derivation tags a zero-yield bare-question user turn’s episode interrogative_turn (demote-not-delete hint, mirrors the transient tag); the comprehensive gather drops the current question’s exact echo (episode/candidate) plus zero-yield episodes/candidates from the discovery rank list (they stay in evidence_by_id) and adds a CURRENT-facts floor (_render_current_facts, seed-relevant + profile slots, slot round-robin, capped) for owner-referencing queries; the bundle use case score-demotes at hydrate time by zero-yield (primary) or the tag/text-shape hint (application/interrogative_text.py, secondary); the CAPPED fetch readers (FTS episodes arm + pgvector scan, consolidations_repository.py) exclude zero-yield rows IN SQL so echoes never occupy fetch slots (under hnsw.iterative_scan the vector fetch refills with real content); and the answer-context render’s per-item truncation is query-aware (http_handlers._truncate_for_budget_query_aware) so a long line keeps the region a query token lives in. SAGE reinforcement edges (rel_sage_*) never count as yield — they attach to the TRIGGERING turn, not to content it taught. A question-shaped turn whose event DID derive records is content-backed and never demoted by the primary signal. Facts/relations are never tagged or demoted. Off → prior behavior, byte-identical; no data changes either way.

Served-path hydration is fault-tolerant (Stage 0a, RFC dapper-snuggling-hare)

Section titled “Served-path hydration is fault-tolerant (Stage 0a, RFC dapper-snuggling-hare)”

The served /v2/retrieve compat builder (_build_compat_bundle / _hydrate_compat_evidence_row in adapters/inbound/http_handlers.py) hydrates each support row via inspect_object/inspect_provenance. A row the legacy inspector cannot load — a relation (no relation loader in ConsolidationsRepository.get, raising NotFoundError→404) or a provenance-less object (raising FailedPreconditionError→412) — is caught for exactly those two typed errors and skipped, identical to the profile/FactRoute hydrate-and-filter (retrieval_route_executors.py:707-710), so one un-loadable edge can no longer fail the whole request. PermissionDenied/Unauthenticated/Unavailable still propagate. Skips emit one PII-free retrieval.compat_hydration_skipped event (counts by kind, never object ids). Alert: WARN when hydrated_count == 0 AND skipped_total > 0 (every piece of evidence was dropped — a present-but-unhydratable answer reads as a denial). A standing nonzero relation skip count is the signal that Stage 1 (a real relation loader) is still owed.

Sibling-slot consult — hint, not gate (Stage 2, ADR 0168)

Section titled “Sibling-slot consult — hint, not gate (Stage 2, ADR 0168)”

The FactRoute slot router is a HINT, not a GATE. A precise question can lexically route to a slot that holds no current grounded fact while a SIBLING slot holds the grounded answer — the canonical case is “where do I work?” routing to work_site (empty / extraction-noise only; every row superseded) while the grounded current fact lives in employer (e.g. Sodexo). In FactRouteExecutor.execute, the count == 0 MISS branch now calls _consult_sibling_slots before abstaining: it reads the declared sibling via the SAME store.list_current_facts_for_slot reader (no new store method) and, when the sibling holds EXACTLY ONE current grounded fact, returns it through the existing _build_grounded_response (same hydration + real confidence_state). The consult map _SIBLING_SLOT_CONSULT is intentionally tight and one-directional — work_site → employer only — so a genuine work_site question still tries work_site FIRST and only borrows the employer fact on a miss; an employer question never falls back to work_site (no reverse edge); a sibling with two or more current facts is NOT borrowed (an ambiguity must not masquerade as a precise answer); role is deliberately excluded (wrong-axis, multi-valued). When neither the primary nor the sibling has a grounded fact, the executor still returns the honest not_known for the PRIMARY slot, so the abstention contract is unchanged. A rescued miss emits one PII-free retrieval.fact_sibling_consult event (slot names only — never owner_id, raw query, or value_text; INV-006). Default ON, gated by MEMLINK_HYBRID_FACT_SIBLING_CONSULT_ENABLED (flip-off → byte-identical; flipping live needs a memlink rebuild per HARD RULE 6). Source: memlink-hybrid/src/memlink_hybrid/application/retrieval_route_executors.py (_SIBLING_SLOT_CONSULT, _consult_sibling_slots, the execute count==0 branch) and the reader’s currency predicate in memlink-hybrid/src/memlink_hybrid/adapters/outbound/consolidations_repository.py (is_current=TRUE AND status='current' AND superseded_at IS NULL).

Relation graph expansion (operator-deployed ON)

Section titled “Relation graph expansion (operator-deployed ON)”
  • MEMLINK_HYBRID_RELATION_EXPANSION_ENABLED (on) — relation graph expansion (off / on / shadow).
  • MEMLINK_HYBRID_RELATION_EXPANSION_QUERY_GATE (on) — gate expansion to relational query classes.
  • MEMLINK_HYBRID_RELATION_SEED_BLOCKING (on) — embedding + FTS relation-seed resolution.
  • MEMLINK_HYBRID_RELATION_EXPANSION_MAX_HOPS (operator-deployed 2) — 1 = single-hop UNION; 2 activates the recursive-CTE second hop.
  • MEMLINK_HYBRID_RELATION_GRAPH_DISTANCE_RERANK (on) — per-hop relation-bonus decay.
  • MEMLINK_HYBRID_RELATION_TEMPORAL_FILTER (on) — bitemporal current-only edge filter (needs migration 0084).
  • MEMLINK_HYBRID_RELATION_VALID_AT_ON_WRITE (default-off; SAGE Phase 2, RFC 0097) — stamp valid_at = created_at on new edges in persist_relations so as-of-T queries have a validity lower bound (additive; needs migration 0084). The as-of-T read itself is PostgresFoundationStore.list_relations_for_owner_as_of(owner_id, as_of) (Phase 2b): the edges valid at instant T — [valid_at, invalid_at) contains T — regardless of current status, so a contradiction (old edge closed via supersede_relations_by_object_ids setting invalid_at, new edge opened) is answered correctly before vs after T. Read-only, owner-scoped, inert (no live caller).
  • MEMLINK_HYBRID_RELATION_EDGE_DEDUP (on) — canonical-entity expansion-edge dedup.
  • MEMLINK_HYBRID_PPR_READER_ENABLED (default-off; RFC 0097 Phase 3, mission C) — wires the Personalized-PageRank associative reader (application/ppr_reader.py) into the live search_support as an ADDITIVE, default-off, shadow-first candidate source (off / on / shadow) fused via the EXISTING RRF (equal-weight, the 5th rank list). off early-skips — no owner-edge fetch, no PPR, byte-identical to today. Query-class gated by the same MEMLINK_HYBRID_RELATION_EXPANSION_QUERY_GATE; seeds resolve via the same _find_seed_entities; candidates hydrate + score through the same expansion block (no new scorer/traverser). Surfaces multi-hop associative bridges a 1-hop expansion misses. No migration. Augments — never replaces — relation expansion (both contribute rank lists to RRF when on).
  • MEMLINK_HYBRID_CHAT_IMPORT_ENABLED (default-off; SAGE Phase 5, RFC 0097) — gates a chat-backup importer wet-run (live ingest) in application/load_chat_import.py::LoadChatImportUseCase. A dry-run (parse + ISO-8601 timestamp normalization + chunk, no side effects) always works; a wet-run is refused unless this flag is set. Imports carry the chat_import ingest profile to distinguish them from chat_realtime traffic.
  • MEMLINK_HYBRID_SAGE_FEEDBACK_ENABLED (default-off; SAGE Phase 4, RFC 0097) — pragmatic-SAGE reader→writer feedback loop. The pure reader (application/sage_reader_signals.py) reads world.retrieval_feedback state + the owner’s current edges and emits candidate signals (frequent_entity / weak_link / queried_but_missing); the pure writer (application/sage_writer_proposals.py) turns them into PROPOSED-but-never- persisted relation candidates. Off → the reader emits nothing and the loop is a no-op. No Postgres write, no graph mutation — proposals are operator-gated.
  • MEMLINK_HYBRID_SAGE_SHADOW (default-off; SAGE Phase 4, RFC 0097) — shadow-first marker stamped on emitted SAGE signals/proposals (telemetry-only; nothing acts on them either way, since the writer is proposal-only).
  • MEMLINK_HYBRID_SAGE_WRITE_ENABLED (default-off; SAGE Phase 6 / Stage 2, RFC 0097 / ADR 0152) — runtime closure: after a user turn the derivation carrier (dispatch_derivation._sage_reinforce_best_effortsage_writer_persist) persists gated, tagged SAGE proposals into the owner’s live graph, so memory self-improves while chatting (reinforced edges immediately change retrieval). Live, not shadow — kept safe by: every edge tagged metadata.origin=sage_reinforcement (one-call revert via supersede_relations_by_object_ids), a confidence floor + per-pass cap (a trickle), owner-scoped, supersede-not-delete. Needs SAGE_FEEDBACK_ENABLED for the reader to emit; best-effort (never breaks derivation). Off → no write. Consolidation-path reinforcement (SAGE Phase 6, RFC 0097 / ADR 0156): the same flag (no new flag) also drives reinforcement from the Luna consolidation pass (INV-022 Surface A). After the living-document write, RunLunaConsolidationUseCase.execute calls the store carrier reinforce_owner_graph_best_effort(owner_id, source_event_id=None, now), which reuses sage_reinforce_from_store verbatim under owner_scope(owner_id) (the per-tx owner GUC, INV-022 rule 4) in its own persist_relations transaction (two-module/two-transaction separation, INV-022 rule 5). Best-effort at both layers — a reinforcement failure never fails the consolidation. The response carries an additive reinforced_edge_count (default 0); the use case emits one structured luna.sage_reinforce event. So the owner graph reinforces both per turn and on the consolidation schedule.
  • application/sage_shadow_pass.py::run_sage_shadow_pass(owner_id, feedback, edges, …) (SAGE Phase 6) — the per-owner orchestration a future, gated, scheduled Luna-consolidation driver will invoke: runs the reader→writer loop for ONE owner and packages an owner-tagged ShadowPassReport of proposals it would open a PR with. No write/mutation/PR/I/O; enabled mirrors MEMLINK_HYBRID_SAGE_FEEDBACK_ENABLED. The PPR shadow reader’s owner-scope bleed defense is pinned by tests/test_ppr_owner_scope_isolation.py.

Scope authentication (RFC 0091 / ADR 0143 / ADR 0146)

Section titled “Scope authentication (RFC 0091 / ADR 0143 / ADR 0146)”
  • MEMLINK_HYBRID_SCOPE_AUTH_MODE (enforce on the operator deployment; compat_static_scope on the shared/client gateways) — scope-auth enforcement posture (disabled / compat_static_scope / enforce).
  • MEMLINK_HYBRID_SCOPE_AUTH_PER_TENANT_ENABLED (on) — per-owner signing-key resolver / prontera /verify oracle (falls back to the shared key).
  • MEMLINK_HYBRID_SCOPE_AUTH_PER_TENANT_REQUIRED (operator-deployed 1) — final cutover: retire the shared key for a provisioned owner.
  • MEMLINK_HYBRID_SCOPE_AUTH_ASYMMETRIC (on) — accept Ed25519 (asymmetric) request signatures verified against the prontera-published JWKS.

Luna chat-learning rollout (operator dry-run + gated backfill)

Section titled “Luna chat-learning rollout (operator dry-run + gated backfill)”

The operator-facing safe-rollout layer for turning Luna consolidation on per tenant (ADR 0183; runbook docs/operations/luna-chat-learning-rollout-runbook.md).

  • Emission. Luna distils a tenant’s agent_self_events into a per-owner living document (INV-022 Surface A — run_luna_consolidation). The live chat -> self-event emission rule present today is the self_memory_only assistant-snapshot reflection (memlink http_handlers + _truncate_for_self_memory_summary); growing self-memory is done going-forward, not by an ad-hoc historical bulk write.
  • Cadence / governance. Enablement reuses existing toggles — the global kill switch luna_auto_enabled (runtime config.py; env MOTH_HYBRID_LUNA_AUTO_ENABLED, legacy …_MORPHEUS_AUTO_ENABLED) and the per-scope user_settings_profile.morpheus_auto_enabled (physical name retained; conceptually Luna). Runs are bounded by the per-owner per-day quota luna_max_runs_per_day (default 50). No new flag was added.
  • Dry-run (no persist). RunLunaConsolidationUseCase.build_consolidation_preview runs the same pre-persist gates + _build_llm_overrides render but never calls store.run_luna_consolidation; CLI scripts/luna_chat_consolidation_dryrun.py (--confirm-gated, cgroup-capped) shows the would-write body for a tenant writing nothing.
  • Backfill (deferred). scripts/luna_chat_self_event_backfill.py is a read-only planner: a strict NO-OP without --i-understand-bulk-write, REFUSEs without --i-am-under-cgroup-cap, and defers the actual write to going-forward emission (warn-before-bulk; append-only self-events have no dedup).
  • Update this page when memlink APIs, storage schemas, retrieval behavior, or runtime memory integration changes.
  • Schema-dependent code changes must follow the migration-before-code invariant.
  • When a MEMLINK_HYBRID_* flag is added to config.py or the retrieval adapter, name it in the Flags & Rollout Inventory above (or the test’s wiki operational-config allowlist) or tests/test_flag_inventory.py fails the build.
  • This page does not list every migration or endpoint.