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.
Ingestion
Section titled “Ingestion”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.
Model Map
Section titled “Model Map”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.
Evaluation Harness (accuracy foundation)
Section titled “Evaluation Harness (accuracy foundation)”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.
Flags & Rollout Inventory
Section titled “Flags & Rollout Inventory”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.
Extraction & derivation
Section titled “Extraction & derivation”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 emitworld.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 leftworld.relationsnear-empty.MEMLINK_HYBRID_RECONCILE_DECISION_ENABLED(on) — emitreconcile_opdecision attributes (supersede-not-delete bookkeeping).MEMLINK_HYBRID_EMBEDDING_ENABLED(on) — embedding generation write path (provider key brokered).MEMLINK_HYBRID_LLM_TIMEOUT_MS(code default4500; operator-deployed60000= 60 s) — non-embedding LLM call timeout.MEMLINK_HYBRID_LUNA_CONSOLIDATION_ENABLED(off; legacy fallbackMEMLINK_HYBRID_MORPHEUS_LLM_CONSOLIDATION_ENABLEDstill 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 inworld.extraction_outage_log; afterMEMLINK_HYBRID_EXTRACTION_FAILOVER_BRIDGE_THRESHOLD_SECONDS(default 5h, Mark’s directive) the deterministic extractor switches back on PER-CALL as a temporary bridge (facts markedingest_profile='deterministic_bridge'); on recovery V4-Flash resumes from the per-ownerworld.extraction_watermarkand force-re-extracts the outage gap so it replaces the bridge facts. The worker probes V4-Flash everyMEMLINK_HYBRID_EXTRACTION_FAILOVER_PROBE_INTERVAL_SECONDS(default 60s) to detect recovery and defers a queued job forMEMLINK_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 defersderive_windowjobs and/v2/window-backfill/start404s.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 dispatchesderive_event, also enqueue onederive_windowjob 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 theworks_at/lives_ingraph 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) intoworld.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).
Retrieval routing & readers
Section titled “Retrieval routing & readers”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_site→employer) before abstaining, so “where do I work?” surfaces the grounded employer fact (e.g. Sodexo) instead ofnot_known. Defaults ON (operator ship-live; pure-additive — only ever turns a primary miss into a grounded sibling answer or leaves the honestnot_knownuntouched). Set to0for the primary-miss →not_knownbehaviour.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-freerecall.comprehensive_shadowcompare 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 episodeinterrogative_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 inevidence_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 (underhnsw.iterative_scanthe 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-deployed2) —1= single-hop UNION;2activates 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) — stampvalid_at = created_aton new edges inpersist_relationsso as-of-T queries have a validity lower bound (additive; needs migration 0084). The as-of-T read itself isPostgresFoundationStore.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 viasupersede_relations_by_object_idssettinginvalid_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 livesearch_supportas an ADDITIVE, default-off, shadow-first candidate source (off/on/shadow) fused via the EXISTING RRF (equal-weight, the 5th rank list).offearly-skips — no owner-edge fetch, no PPR, byte-identical to today. Query-class gated by the sameMEMLINK_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) inapplication/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 thechat_importingest profile to distinguish them fromchat_realtimetraffic.
SAGE self-evolution (RFC 0097 / ADR 0150)
Section titled “SAGE self-evolution (RFC 0097 / ADR 0150)”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) readsworld.retrieval_feedbackstate + 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_effort→sage_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 taggedmetadata.origin=sage_reinforcement(one-call revert viasupersede_relations_by_object_ids), a confidence floor + per-pass cap (a trickle), owner-scoped, supersede-not-delete. NeedsSAGE_FEEDBACK_ENABLEDfor 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.executecalls the store carrierreinforce_owner_graph_best_effort(owner_id, source_event_id=None, now), which reusessage_reinforce_from_storeverbatim underowner_scope(owner_id)(the per-tx owner GUC, INV-022 rule 4) in its ownpersist_relationstransaction (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 additivereinforced_edge_count(default 0); the use case emits one structuredluna.sage_reinforceevent. 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-taggedShadowPassReportof proposals it would open a PR with. No write/mutation/PR/I/O;enabledmirrorsMEMLINK_HYBRID_SAGE_FEEDBACK_ENABLED. The PPR shadow reader’s owner-scope bleed defense is pinned bytests/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(enforceon the operator deployment;compat_static_scopeon 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/verifyoracle (falls back to the shared key).MEMLINK_HYBRID_SCOPE_AUTH_PER_TENANT_REQUIRED(operator-deployed1) — 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_eventsinto a per-owner living document (INV-022 Surface A —run_luna_consolidation). The live chat -> self-event emission rule present today is theself_memory_onlyassistant-snapshot reflection (memlinkhttp_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(runtimeconfig.py; envMOTH_HYBRID_LUNA_AUTO_ENABLED, legacy…_MORPHEUS_AUTO_ENABLED) and the per-scopeuser_settings_profile.morpheus_auto_enabled(physical name retained; conceptually Luna). Runs are bounded by the per-owner per-day quotaluna_max_runs_per_day(default 50). No new flag was added. - Dry-run (no persist).
RunLunaConsolidationUseCase.build_consolidation_previewruns the same pre-persist gates +_build_llm_overridesrender but never callsstore.run_luna_consolidation; CLIscripts/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.pyis 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).
Maintenance Notes
Section titled “Maintenance Notes”- 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 toconfig.pyor the retrieval adapter, name it in the Flags & Rollout Inventory above (or the test’s wiki operational-config allowlist) ortests/test_flag_inventory.pyfails the build.
Known Unknowns
Section titled “Known Unknowns”- This page does not list every migration or endpoint.

