Skip to main content

Self-Improving Memory

Memory consolidation is the analogue of slow-wave sleep: background maintenance that prunes weak traces, merges duplicates, strengthens co-activated patterns, derives new insights, compacts old episodic traces, and rebuilds the search index.


Overview

The self-improvement subsystem has three components that work together:

ComponentPurposeLLM Required
ConsolidationLoop6-step background maintenance pipelineOnly for the derive step
RetrievalFeedbackSignalDetect which retrieved traces were used vs ignored by the LLMNo
ObservationCompressor + ObservationReflectorCompress conversations into dense memory tracesYes

ConsolidationLoop

Source: memory/consolidation/ConsolidationLoop.ts

The consolidation pipeline runs 6 ordered steps in a single cycle. A boolean mutex prevents concurrent runs --- if run() is called while a cycle is in progress, it returns immediately with zero counts.

Step 1: Prune

Soft-delete traces whose Ebbinghaus strength has decayed below the pruneThreshold (default: 0.05).

S(t) = S0 * e^(-dt / stability)

If S(t) < pruneThreshold → trace.deleted = 1

Emotional memories (intensity > 0.3) are protected from pruning regardless of strength.

Step 2: Merge

Deduplicate near-identical traces. Two comparison strategies:

MethodActivationThreshold
Embedding cosine similarityWhen embedFn is providedDefault 0.95
Exact content-hash comparisonFallback when no embeddingsSHA-256 match

When a merge occurs:

  • The newer trace's content is kept.
  • Tags from both traces are unioned.
  • The older trace is soft-deleted with a reference to the survivor.

Step 3: Strengthen

Read retrieval-feedback co-usage signals from the retrieval_feedback table and record Hebbian co-activation edges in the memory graph.

Traces that were retrieved together for the same query get CO_ACTIVATED edges. The learning rate (default 0.1) controls how quickly edge weights grow. This implements the Hebbian rule: "neurons that fire together wire together."

Step 4: Derive (LLM-backed)

Detect clusters of related memories using the memory graph's detectClusters() method (minimum cluster size: 5). For each cluster, invoke an LLM to synthesise a higher-level insight trace.

  • The derived trace has type: 'semantic' and contains the cluster's distilled knowledge.
  • SCHEMA_INSTANCE edges connect the original traces to the derived insight.
  • Guarded by maxDerivedPerCycle (default: 5) to prevent unbounded graph growth.
  • Skipped entirely when no llmInvoker function is provided.

Step 5: Compact

Promote old, high-retrieval episodic traces to semantic type. This is a lightweight episodic-to-semantic migration:

CriterionThreshold
Age> 7 days (604,800,000 ms)
Retrieval count>= 3

Compacted traces keep their content but change type from episodic to semantic, reflecting that frequently-accessed episodic memories have been consolidated into general knowledge.

Step 6: Re-index

Rebuild the FTS5 full-text search index over memory_traces and log the consolidation run to the consolidation_log table with counts and duration.


Running Consolidation

Programmatic

import { Memory } from '@framers/agentos';

const mem = new Memory({
path: './brain.sqlite',
selfImprove: true,
consolidation: {
trigger: 'interval',
every: 3_600_000, // Run every hour
pruneThreshold: 0.05,
mergeThreshold: 0.95,
},
});

// Manual trigger
const result = await mem.consolidate();
console.log(`Pruned: ${result.pruned}`);
console.log(`Merged: ${result.merged}`);
console.log(`Derived: ${result.derived}`);
console.log(`Compacted: ${result.compacted}`);
console.log(`Duration: ${result.durationMs}ms`);

CLI

wunderland memory consolidate
wunderland memory health # Shows last consolidation timestamp + stats

Trigger Modes

ModetriggereveryBehaviour
Timer'interval'MillisecondsRuns on a wall-clock interval (default: 1 hour)
Turn-based'turns'Turn countRuns after every N conversation turns
Manual'manual'IgnoredOnly runs when consolidate() is called explicitly
// Turn-based: consolidate every 50 conversation turns
const mem = new Memory({
path: './brain.sqlite',
selfImprove: true,
consolidation: {
trigger: 'turns',
every: 50,
},
});

// Manual only
const mem2 = new Memory({
path: './brain.sqlite',
selfImprove: true,
consolidation: {
trigger: 'manual',
},
});

ConsolidationResult

interface ConsolidationResult {
pruned: number; // Traces soft-deleted (below strength threshold)
merged: number; // Trace pairs merged into single traces
derived: number; // New insight traces synthesised from clusters
compacted: number; // Episodic traces promoted to semantic
durationMs: number; // Wall-clock time of the consolidation cycle
}

Retrieval Feedback Signal

Source: memory/feedback/RetrievalFeedbackSignal.ts

After the LLM generates a response, the feedback signal detects which injected memory traces were actually referenced vs ignored.

Detection Heuristic

  1. Extract unique keywords (> 4 characters) from each trace's content.
  2. Check how many of those keywords appear in the LLM's response text.
  3. If matchRatio > 0.30 the trace is marked 'used', otherwise 'ignored'.

An optional similarityFn can be injected for higher-fidelity semantic detection.

Effect on Decay

SignalAction
'used'updateOnRetrieval() --- increases strength and stability, doubles reinforcement interval
'ignored'penalizeUnused() --- accelerates decay for repeatedly-ignored traces

Persistence

Each feedback event is written synchronously to the retrieval_feedback table in the agent's brain database:

CREATE TABLE retrieval_feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trace_id TEXT NOT NULL REFERENCES memory_traces(id),
signal TEXT NOT NULL, -- 'used' or 'ignored'
query TEXT, -- The query that triggered retrieval
created_at INTEGER NOT NULL
);

API

// Detect and record feedback automatically
const feedback = mem.feedback(injectedTraces, llmResponse, query);
// feedback: RetrievalFeedback[] — one per injected trace

Observational Compression

The observation system compresses long-running conversations into dense, searchable memory traces. Three tiers:

Recent Messages (raw conversation turns)
→ Observations (concise notes via ObservationCompressor, 3-10x compression)
→ Reflections (long-term traces via ObservationReflector, 5-40x compression)

ObservationCompressor

Source: memory/observation/ObservationCompressor.ts

Groups related observation notes by topic/entity overlap, produces 1-3 sentence summaries per group, assigns priority levels (critical, important, informational), and attaches three-date temporal metadata.

Personality bias: When HEXACO traits are provided, the system prompt is tuned to emphasise categories aligned with the agent's personality:

High TraitObserver Focus
EmotionalityEmotional shifts, tone changes, sentiment transitions
ConscientiousnessCommitments, deadlines, action items, structured plans
OpennessCreative tangents, novel ideas, exploratory topics
AgreeablenessUser preferences, rapport cues, communication style
HonestyCorrections, retractions, contradictions

ObservationReflector

Source: memory/observation/ObservationReflector.ts

Consolidates accumulated observation notes into long-term MemoryTrace objects. Conflict resolution is personality-driven:

PersonalityResolution Strategy
High Honesty (> 0.6)Prefer newer information, supersede old traces
High Agreeableness (> 0.6)Keep both versions, note discrepancy
DefaultPrefer higher confidence (only if difference > 0.2)

Temporal Reasoning

The three-date model tracks temporal context for every memory:

DateFieldPurpose
Observed atobservedAtWhen the memory was recorded by the system
Referenced atreferencedAtWhen the event the memory refers to actually occurred
Relative labelrelativeLabelHuman-friendly label: "just now", "earlier today", "last Tuesday", "2 weeks ago"

The relativeTimeLabel() function converts Unix-ms timestamps into natural-language labels. Labels are computed relative to the current time and cover granularities from seconds ("just now") through months ("last month") to years ("2 years ago").


Configuration Reference

OptionDefaultDescription
consolidation.trigger'interval'What triggers a consolidation run
consolidation.every3_600_000Interval (ms) or turn count depending on trigger
consolidation.pruneThreshold0.05Strength below which traces are pruned
consolidation.mergeThreshold0.95Cosine similarity above which traces are merged
consolidation.deriveInsightstrueWhether to synthesise insight traces from clusters
consolidation.maxDerivedPerCycle10Max new insight traces per cycle
consolidation.maxTracesPerCycle500Max traces processed per cycle
consolidation.minClusterSize5Min cluster size for schema integration

Source Files

FilePurpose
memory/consolidation/ConsolidationLoop.ts6-step consolidation pipeline
memory/feedback/RetrievalFeedbackSignal.tsUsed/ignored detection + decay modulation
memory/observation/ObservationCompressor.tsLLM-backed 3-10x compression
memory/observation/ObservationReflector.tsLLM-backed reflection + conflict resolution
memory/observation/temporal.tsThree-date model + relativeTimeLabel()
memory/decay/DecayModel.tsEbbinghaus curve, updateOnRetrieval, penalizeUnused