Aleph
Architecture

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 layerRealised byDecay
Working (current task)Live session / scratchpad + raw items in the session_recent slotn/a (ephemeral)
Episodic (experiences, causal chains)Session summaries, transcript / subagent-* notesRetrieval recency + dream decay
Semantic (facts & rules)Distilled preference / learning / skill / reference notesSeverity-floored confidence decay
Raw (audit / 回溯 base)raw_memories rows behind every distillationRetention 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:

TraitFilePurposePrimary caller
NoteStoresrc/memory/notes/store.rsNotes index, wikilinks, FTS, vector searchNoteFactRetrieval, NoteIndexer
RawMemoryStoresrc/memory/store/raw_memory.rsRaw memory CRUD + is_processed flagCompressionService, SessionCompactor
DreamStoresrc/memory/store/mod.rsDream status + daily insights + SkillOpt verdictDreamDaemon
CompressionStoresrc/memory/store/mod.rsCompression-run audit metadataCompressionService

All four are implemented by SqliteMemoryBackend, wrapped as MemoryBackend = Arc<SqliteMemoryBackend>.

Core Tables

TablePurpose
raw_memoriesSession transcripts, attachment text, is_processed flag
notes_indexKnowledge note metadata (title, category, tags, hash)
notes_linksBidirectional wikilink graph
notes_ftsFTS5 full-text index over note content
notes_vec_mapLinks (path, agent_id) to numeric rowids
notes_vec_{dim}sqlite-vec virtual tables (768/1024/1536 dims)
memory_eventsImmutable event log (event-sourced mutations)
context_anchorsFact-to-session linkage for provenance
dream_statusPer-run tracking
dream_reportsPer-cycle activity counters + serialized EvolutionOutcome (SkillOpt verdict)
daily_insightsDream daemon output cache
recall_signalsPer-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 list

Reciprocal Rank Fusion (RRF): RRF_score = Σ 1 / (k + rank_i), with k = 60.

Scoring Pipeline

StageDescriptionDefault
cosine_rerankBlend vector-search score with fresh cosine similarityenabled
recency_boostAdditive boost for recently created facts+0.1
length_normalizationPenalize very long contentenabled
time_decayExponential decay by age, floor 0.5half-life 30 days
hard_min_scoreDrop candidates below threshold0.35
mmr_diversityMaximal Marginal Relevance — defer near-duplicates to tailλ = 0.5
reinforcementHot-surfacing from recall_signals countsenabled

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 = true

Compression 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:

  1. config.enabled is true
  2. is_within_window() (default 02:0005:00 local)
  3. idle_seconds() >= idle_threshold_seconds (default 900 s)
  4. is_running compare-exchange latch is free
  5. should_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

StageDailyWeekly
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 gateevolution/gate.rs::evaluate_gate(candidate, current, best, ε) keeps an edit only when it strictly improves a score (> current + ε, ties rejected). Returns AcceptNewBest / Accept / Reject. At cycle level (mod.rs Phase 5.5) it scores memory_health_score before 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 persistencebest_health is loaded from dream_best_health__{agent} and re-persisted on every AcceptNewBest, 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 distill Supersede action (SkillDistill / FeedbackDistill, via stages::charge_distill_budget). Additive growth (new synthesis notes, distill New / Strengthen, weave links) is not budgeted, so the growth path is never starved.
  • Recall-evidence gateevolution/evidence.rs::gate_supersede_evidence demands the LLM's confidence strictly beat a note's saturating recall support before a destructive Supersede lands (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 remember builtin tool (add / replace / remove, plus an atomic batch action; 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 CuratedMemoryLayer Stable prefix
  • Frozen snapshot — changes require explicit re-curation

Memory Extensions

The pipeline exposes three hook points through MemoryExtension:

HookWhenPurposeDispatch
on_retrieveAfter assembly, before XML renderingAugment / filter / reorder the envelopeSequential, 2 s timeout
on_captureBefore insert_raw_memoryInspect / redact / block raw memoriesChained pipeline, 3 s timeout, Block short-circuits
produceDedicated scheduler tickProduce raw memories from external sourcesParallel 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:

  1. durable preference / identity fact / standing instruction → remember (HOT)
  2. user corrected you → flag_user_correction (self-discovered lessons instead go to note_manage as lesson notes)
  3. reusable domain knowledge → note_manage (DURABLE)
  4. 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

ConcernMitigation
UTF-8 truncationchars().take(n) (never mid-character)
Lock poisoningunwrap_or_else(|e| e.into_inner())
SQL injectionParameterized queries via rusqlite
Vector boundsCosine clamped to [-1.0, 1.0]
Token overflowAssemblyBudget enforces limits

Module Map

PathContents
src/memory/mod.rsModule 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.rsMemoryReflector, 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.rssqlite-vec integration
src/memory/retrieval/Generic retrieval interfaces
src/memory/scoring_pipeline/Scoring stages
src/memory/dreaming/mod.rsDreamDaemon, 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

On this page