Memory Architecture
Two-layer data model (L0 raw / L1 notes), four storage traits, SkillOpt evolution gate, hybrid retrieval, and provenance chain.
Memory Architecture
This page covers the implementation of Aleph's memory system: the L0 (raw) / L1 (notes) two-layer data model, the four storage traits, the dream-daemon consolidation pipeline, the SkillOpt evolution gate, retrieval, extensions, and provenance. For a concept-level overview see Memory Concepts.
Two-Layer Data Model
Gateway / Agent Loop
│
▼
┌─────────────────────────────────────────────────────────────┐
│ raw_memories (SQLite) │
│ sessions · transcripts · attachment_text │
│ consumed by CompressionService (is_processed flag) │
└─────────────────────────────────────────────────────────────┘
│ CompressionService (realtime)
▼
┌─────────────────────────────────────────────────────────────┐
│ notes (Markdown files + SQLite index) │
│ ~/.aleph/memory/note/{agent}/{category}/*.md │
│ notes_index · notes_links · notes_fts · notes_vec_{dim} │
└─────────────────────────────────────────────────────────────┘
│ Dream Daemon (offline, idle-only)
▼
consolidate / drift / synthesis / lint / decay / digest
│
▼
queries: NoteFactRetrieval.retrieve() → ScoredFact<MemoryFact>Gateway writes conversation turns to raw_memories through RawMemoryStore. CompressionService periodically drains unprocessed rows, asks an LLM to extract durable facts, and writes them back as markdown notes under ~/.aleph/memory/note/{agent}/{category}/. NoteIndexer keeps the SQLite side (index, wikilinks, FTS5, per-dimension vec0 tables) in sync on every write. Offline, the Dream Daemon consolidates clusters, resolves drift, synthesizes insights, lints schemas, decays weak notes, and emits digests. Queries flow through NoteFactRetrieval and return ScoredFact<MemoryFact>.
Cognitive-Layer View
The two physical tiers realise a four-layer cognitive model. CognitiveLayer is the canonical labelling, derived (not stored) at render time by assembler::render::cognitive_layer:
| Cognitive layer | Realised by | Decay |
|---|---|---|
| Working (current task) | Live session / scratchpad + raw items in the session_recent slot | n/a (ephemeral) |
| Episodic (experiences, causal chains) | Session summaries, transcript / subagent-* notes | Retrieval recency + dream decay |
| Semantic (facts & rules) | Distilled preference / learning / skill / reference notes | Severity-floored confidence decay |
| Raw (audit / 回溯 base) | raw_memories rows behind every distillation | Retention sweep only |
Retrieved items are stamped with their layer in the assembled memory context (markdown header + XML layer="…" attribute) so the model perceives the structure. Notes flagged permanent: true (or tagged permanent / pinned), and categories in memory.memory_decay.protected_types, are exempt from decay/archival — the "永久核心知识不受影响" guarantee.
Hot-surfacing and time-decay at recall are on by default (memory.retrieval_scoring): frequently-recalled notes bubble up (reinforcement, from recall_signals counts), stale ones fade (recency); both read existing data with no extra model calls.
Embeddings are pluggable via the EmbeddingProvider trait (OpenAI / SiliconFlow / Ollama through the OpenAI-compatible RemoteEmbeddingProvider); the data-stays-local requirement is met today by pointing it at a local Ollama. A bundled in-binary ONNX backend is intentionally not added — it would pull a heavy single-purpose native dependency into core (R3) for a privacy goal the local-Ollama path already satisfies. It remains a clean future addition behind the same trait if the trade-off changes.
Storage Traits
The storage layer uses four separate traits so each caller depends only on what it uses:
| Trait | File | Purpose | Primary caller |
|---|---|---|---|
NoteStore | src/memory/notes/store.rs | Notes index, wikilinks, FTS, vector search | NoteFactRetrieval, NoteIndexer |
RawMemoryStore | src/memory/store/raw_memory.rs | Raw memory CRUD + is_processed flag | CompressionService, SessionCompactor |
DreamStore | src/memory/store/mod.rs | Dream status + daily insights + SkillOpt verdict | DreamDaemon |
CompressionStore | src/memory/store/mod.rs | Compression-run audit metadata | CompressionService |
All four are implemented by SqliteMemoryBackend, wrapped as MemoryBackend = Arc<SqliteMemoryBackend>.
Core Tables
| Table | Purpose |
|---|---|
raw_memories | Session transcripts, attachment text, is_processed flag |
notes_index | Knowledge note metadata (title, category, tags, hash) |
notes_links | Bidirectional wikilink graph |
notes_fts | FTS5 full-text index over note content |
notes_vec_map | Links (path, agent_id) to numeric rowids |
notes_vec_{dim} | sqlite-vec virtual tables (768/1024/1536 dims) |
memory_events | Immutable event log (event-sourced mutations) |
context_anchors | Fact-to-session linkage for provenance |
dream_status | Per-run tracking |
dream_reports | Per-cycle activity counters + serialized EvolutionOutcome (SkillOpt verdict) |
daily_insights | Dream daemon output cache |
recall_signals | Per-note access counters driving reinforcement |
sqlite-vec Virtual Tables
CREATE VIRTUAL TABLE notes_vec_768 USING vec0(embedding float[768]);
CREATE VIRTUAL TABLE notes_vec_1024 USING vec0(embedding float[1024]);
CREATE VIRTUAL TABLE notes_vec_1536 USING vec0(embedding float[1536]);notes_vec_map bridges human-readable (path, agent_id) pairs to the numeric rowids vec0 requires.
Retrieval Pipeline
NoteFactRetrieval
#[async_trait]
pub trait NoteFactRetrieval: Send + Sync {
async fn retrieve(
&self,
query: &str,
agent_id: &str,
limit: usize,
) -> Result<Vec<ScoredFact<MemoryFact>>, AlephError>;
}Hybrid Search (Vector + FTS)
Query
├── Vector Search (sqlite-vec) → candidates with distance scores
├── FTS Search (FTS5) → candidates with match scores
└── RRF Fusion → unified ranked listReciprocal Rank Fusion (RRF): RRF_score = Σ 1 / (k + rank_i), with k = 60.
Scoring Pipeline
| Stage | Description | Default |
|---|---|---|
cosine_rerank | Blend vector-search score with fresh cosine similarity | enabled |
recency_boost | Additive boost for recently created facts | +0.1 |
length_normalization | Penalize very long content | enabled |
time_decay | Exponential decay by age, floor 0.5 | half-life 30 days |
hard_min_score | Drop candidates below threshold | 0.35 |
mmr_diversity | Maximal Marginal Relevance — defer near-duplicates to tail | λ = 0.5 |
reinforcement | Hot-surfacing from recall_signals counts | enabled |
memory.retrieve_with_trace returns the per-stage StageTrace (telemetry) so a debugging panel can see exactly where a fact was boosted, demoted, or dropped. The trace observes the same pipeline production runs.
Background Processing
Compression Service
Runs in real-time as raw memories arrive:
Raw Memories (is_processed = false)
→ SessionCompactor: per-session compaction
→ NoteIndexer: create/update knowledge notes
→ mark is_processed = trueCompression scheduling lives under [policies.memory.compression] (src/config/types/policies/memory.rs): turn threshold, hourly background interval, session-end flush, correction flush (flag_user_correction), and the memory.compress RPC. The earlier idle-timeout trigger was removed — it had no reachable production path.
Dream Daemon
src/memory/dreaming/mod.rs. ensure_dream_daemon installs one daemon per process via OnceCell; no-ops when memory / dreaming is disabled or in cfg!(test). The daemon ticks every DEFAULT_CHECK_INTERVAL_SECONDS = 60 and runs when:
config.enabledis trueis_within_window()(default02:00–05:00local)idle_seconds() >= idle_threshold_seconds(default 900 s)is_runningcompare-exchange latch is freeshould_skip_scheduled_run(once-per-day short-circuit) allows it
Which pipeline runs is decided per cycle by the signal-driven StrategySelector + MutationGate (Consolidate / Synthesize / Conserve). Runs are wrapped in tokio::time::timeout(max_duration_seconds) (default 600 s). Every stage is interruption-aware — ctx.activity_checker is consulted before each execute; on user activity, status = Interrupted, interrupted_at_stage names the aborting stage, and the pipeline returns.
Pipeline Stages
| Stage | Daily | Weekly |
|---|---|---|
NoteConsolidate | ✓ | ✓ |
NoteDrift | ✓ | ✓ |
NoteSynthesis | — | ✓ |
NoteLint | ✓ | ✓ |
NoteDecay | ✓ | ✓ |
FeedbackDistill | ✓ | ✓ |
DailyDigest | ✓ | ✓ |
SkillOpt Evolution Gate (src/memory/dreaming/evolution/)
Ported from SkillOpt (arXiv 2605.23904): a bounded, validation-gated edit loop layered over the LLM-proposed edits (R7: the content is the model's; this layer only enforces discipline).
- Strict-improvement gate —
evolution/gate.rs::evaluate_gate(candidate, current, best, ε)keeps an edit only when it strictly improves a score (> current + ε, ties rejected). ReturnsAcceptNewBest/Accept/Reject. At cycle level (mod.rsPhase 5.5) it scoresmemory_health_scorebefore vs after this cycle's edits; a degrading cycle arms a 2-cycle Conserve cooldown (it does not roll back — edits are already on disk). - Best-checkpoint persistence —
best_healthis loaded fromdream_best_health__{agent}and re-persisted on everyAcceptNewBest, so the honest historical best survives a restart instead of resetting to 0. - Edit budget ("textual learning rate") —
evolution/budget.rs::EditBudget(default 32 edits / 256 KiB) is shared across the destructive stages —NoteConsolidate(merges),NoteDecay(archival), and the distillSupersedeaction (SkillDistill/FeedbackDistill, viastages::charge_distill_budget). Additive growth (new synthesis notes, distillNew/Strengthen, weave links) is not budgeted, so the growth path is never starved. - Recall-evidence gate —
evolution/evidence.rs::gate_supersede_evidencedemands the LLM's confidence strictly beat a note's saturating recall support before a destructiveSupersedelands (production recall is Aleph's cheap stand-in for a held-out split). - Rejected-edit buffer — rejected supersedes are fingerprinted and stored as
DistillRejectRecords (distill_rejects__{agent}, backward-compatible with the legacy fingerprint-only list). They both drop re-proposals in code (stages/mod.rs::gate_action_evidence) and are replayed into the next distill prompt as negative feedback (stages/mod.rs::render_rejected_block) so the model stops re-proposing losing edits.
The cycle-level gate outcome (EvolutionOutcome) is persisted to dream_reports.evolution_json and surfaced via dreaming.list_insights. Legacy facts_* / nodes_* / edges_* columns from the pre-notes schema were dropped by migrate_dream_reports_drop_legacy_cols.
Working Memory Assembly
HybridAssembler (src/memory/assembler/) replaces the legacy ContextComptroller::arbitrate. It produces a MemoryEnvelope with explicit slots:
pub struct MemoryEnvelope {
pub schema_version: String,
pub generated_at: i64,
pub query: String,
pub agent_id: String,
pub session_id: Option<String>,
pub slots: Vec<EnvelopeSlot>, // each with a SlotKind
pub meta: EnvelopeMeta,
}
pub enum SlotKind {
UserProfile,
SessionRecent,
RelevantNotes,
Feedback,
RawFragments,
Nudges,
}
#[async_trait]
pub trait WorkingMemoryAssembler: Send + Sync {
async fn assemble(
&self,
query: &str,
agent_id: &str,
session_id: Option<&str>,
budget: AssemblyBudget,
filter: FactSourceFilter,
) -> Result<MemoryEnvelope, AlephError>;
}HybridAssembler: calls NoteFactRetrieval::retrieve, optionally runs LLM re-ranking (AiProviderReranker), hydrates into EnvelopeItems, applies registered MemoryExtension::on_retrieve hooks, renders the envelope to XML.
Assembly Budget
pub struct AssemblyBudget {
pub max_tokens: usize, // Target token budget for assembled memory
pub min_score: f32, // Minimum fact score threshold
pub max_facts: usize, // Maximum number of facts to include
}Curated Hot Memory (remember)
src/memory/curated/ — a manually-curated, frozen snapshot of critical facts that bypasses normal retrieval:
pub struct CuratedHotMemory {
pub facts: Vec<CuratedFact>,
pub frozen_at: i64,
}
pub struct CuratedFact {
pub content: String,
pub priority: u8, // 1-10, higher = more important
pub source: String,
}- Sole writer: the
rememberbuiltin tool (add/replace/remove, plus an atomicbatchaction; char budget validated on the final state only) - Stored in
~/.aleph/memory/curated/{agent_id}.json - Injected into every prompt unconditionally (up to token budget) as the
CuratedMemoryLayerStable prefix - Frozen snapshot — changes require explicit re-curation
Memory Extensions
The pipeline exposes three hook points through MemoryExtension:
| Hook | When | Purpose | Dispatch |
|---|---|---|---|
on_retrieve | After assembly, before XML rendering | Augment / filter / reorder the envelope | Sequential, 2 s timeout |
on_capture | Before insert_raw_memory | Inspect / redact / block raw memories | Chained pipeline, 3 s timeout, Block short-circuits |
produce | Dedicated scheduler tick | Produce raw memories from external sources | Parallel per-plugin, 30 s timeout |
First-party extensions implement MemoryExtension directly in Rust; third-party plugins implement the same hooks over MCP via McpMemoryExtension. Both register to the same MemoryExtensionRegistry. MemoryProducerScheduler ticks every 10 s, calling produce and routing results through insert_with_capture_filter so producer-generated memories still pass on_capture.
Memory Reflector
pub struct MemoryReflector {
assembler: Arc<dyn WorkingMemoryAssembler>,
llm: Arc<dyn LlmBackend>,
}
impl MemoryReflector {
pub async fn reflect(
&self,
query: &str,
agent_id: &str,
) -> Result<ReflectionResult, AlephError>;
}
pub struct ReflectionResult {
pub answer: String,
pub sources: Vec<MemorySource>,
pub confidence: f32,
}Exposed via the memory_reflect builtin tool. Returns a coherent LLM-synthesised answer with cited sources.
Event Sourcing
Every note mutation is captured as an immutable MemoryEvent (src/memory/events/). Variants cover skeleton mutations (NoteCreated, NoteContentUpdated, NoteInvalidated, NoteRestored, NoteDeleted, NoteConsolidated, NoteMigrated) and pulse observations (NoteAccessed). The MemoryCommandHandler projects events into the notes layer: append to event log → fold events via EventProjector → write markdown → re-index. The event log is the audit source of truth; markdown files are the primary read surface.
Scratchpad
src/memory/scratchpad/ — an in-session working-memory buffer, orthogonal to L0/L1. Per-session and non-archival: when a session ends, the scratchpad is discarded (not compressed into notes). ScratchpadManager writes scratchpad.md under ~/.aleph/workspaces/<agent_id>/ (per-run project overrides do NOT relocate the scratchpad — runtime working memory stays bound to the agent). The ## Plan section is also the agent's execution list (- [ ] / - [~] / - [x] three-state checkboxes), consumed by ExecutionPlanLayer, ScratchpadGoalVerifier, the channel progress push, and the Panel Todo strip.
Correction Rail, Destination Ladder & Acknowledgment Contract
Correction rail (显式纠错链). When the user corrects the model, the flag_user_correction tool writes a RawMemorySource::Correction row at path aleph://correction/{id} (severity-tagged, optional suggested_rule) and kicks an immediate compress→link drain off the critical path — the model's own "the user corrected me" judgement replaces the old keyword SignalDetector (R7). The FeedbackDistill dream stage later reads corrections via the aleph://correction/ path prefix (own feedback_distill watermark on compression_metadata; runs on both the Consolidate and Synthesize strategies) and asks the LLM to pick New / Strengthen / Supersede / Skip per signal — High/Critical severities bypass the batch quorum. The output is feedback/ notes, surfaced two ways: normal relevance retrieval, plus the always-on FeedbackFloorLoader which unconditionally promotes up to 6 High/Critical rules into the envelope's Feedback slot (pre-populated like UserProfile, never dropped by re-rank).
Curated hot zone (remember). The remember tool is the sole writer of the per-agent MEMORY.md hot zone: add / replace / remove, plus an atomic batch action — several operations applied all-or-nothing, with the char budget validated on the final state only and duplicate adds inside a batch skipped idempotently.
Destination ladder (D1). The single authoritative "where does a new memory go" ladder lives in MemoryProtocolLayer (src/thinker/layers/memory_protocol.rs), first matching rung wins:
- durable preference / identity fact / standing instruction →
remember(HOT) - user corrected you →
flag_user_correction(self-discovered lessons instead go tonote_manageaslessonnotes) - reusable domain knowledge →
note_manage(DURABLE) - transient task state → scratchpad, never a memory tool
Update-over-create is preferred throughout.
Acknowledgment contract (D4). Successful writes return a destination receipt. The prompts instruct the model to close its reply with ONE short sentence, in the user's language, saying what was recorded and to which tier — never quoting the stored content back verbatim, and treating the tool's success response as terminal (no repeated writes, no re-echo into another memory tool). This replaces the earlier "silent logging" design.
Safety Properties
| Concern | Mitigation |
|---|---|
| UTF-8 truncation | chars().take(n) (never mid-character) |
| Lock poisoning | unwrap_or_else(|e| e.into_inner()) |
| SQL injection | Parameterized queries via rusqlite |
| Vector bounds | Cosine clamped to [-1.0, 1.0] |
| Token overflow | AssemblyBudget enforces limits |
Module Map
| Path | Contents |
|---|---|
src/memory/mod.rs | Module entry, re-exports |
src/memory/assembler/ | WorkingMemoryAssembler trait, HybridAssembler impl, envelope slots |
src/memory/curated/ | CuratedHotMemory, remember tool integration |
src/memory/extensions/ | MemoryExtension trait, ExtensionRegistry, MCP adapter |
src/memory/reflector/mod.rs | MemoryReflector, ReflectionResult |
src/memory/note_retrieval/ | NoteFactRetrieval trait, hybrid search impl |
src/memory/notes/ | KnowledgeNote, NoteStore, NoteIndexer |
src/memory/store/ | RawMemoryStore, DreamStore, CompressionStore traits + SqliteMemoryBackend |
src/memory/store/sqlite/vec.rs | sqlite-vec integration |
src/memory/retrieval/ | Generic retrieval interfaces |
src/memory/scoring_pipeline/ | Scoring stages |
src/memory/dreaming/mod.rs | DreamDaemon, StrategySelector, MutationGate |
src/memory/dreaming/evolution/ | SkillOpt evolution gate (gate / budget / evidence / score) |
src/memory/dreaming/stages/ | All DreamStage impls |
src/memory/events/ | MemoryEvent, event sourcing |
src/memory/transcript_indexer/ | Transcript chunking and indexing |
src/memory/namespace/ | MemoryNamespace, isolation levels |
src/memory/scratchpad/ | In-session working-memory buffer |
Related Documents
- Memory System Concepts — High-level overview
- Memory Evolution — Spec history (Spec 1-4, A-C)
- Gateway: Memory Methods — API reference
- Thinker — How memory is injected into prompts