Cognitive Memory
Memory benchmarks (full N=500,
gpt-4oandgpt-5-minireaders routed per question type): 85.6% on LongMemEval-S at $0.0090 per correct; withgpt-4oreading every question, the same retrieval scores 84.2%, level with Mastra Observational Memory (84.23%). 70.2% on LongMemEval-M on the 1.5M-token / 500-session haystack variant. Competitive with the strongest published M results in the LongMemEval paper (Wu et al., ICLR 2025: round Top-5 65.7%, session Top-5 71.4%, round Top-10 72.0%). Benchmarks · Run JSONs · SOTA writeup
HEXACO Personality for the trait-by-trait reference covering encoding weights, working-memory capacity, prompt formatting, observer/reflector bias, and runtime self-modification.
Why memory should forget
A pure vector-similarity memory — embed every message, return the cosine-nearest neighbors at retrieval — works for a few thousand turns. Past that scale, undifferentiated retrieval treats every recorded experience as equally available, equally trustworthy, and equally relevant. The cognitive-science literature treats forgetting as the mechanism by which what mattered yesterday continues to matter today, not as a bug to be patched out. AgentOS encodes that principle directly: traces decay, retrieval bias shifts with mood, and consolidation rewrites the store between turns.
The cognitive memory system in AgentOS is built on that argument. Encoding strength is set per-trace, modulated by the personality traits of the agent doing the encoding and by the emotional intensity of the moment (Brown & Kulik, 1977 on flashbulb memories; Yerkes & Dodson, 1908 on the inverted-U arousal curve). Strength then decays exponentially with time on Hermann Ebbinghaus's 1885 forgetting curve S(t) = S₀ · e^(-Δt / stability), and each successful retrieval grows the stability (the desirable-difficulty effect: weaker traces gain more). Working memory is bounded by Baddeley's slot model: seven slots, one more or one fewer by traits, between five and nine. Retrieval composites six signals — vector similarity, current strength, recency, emotional congruence with the agent's mood, graph spreading-activation in the ACT-R tradition (Anderson, 1983), and importance. The graph itself learns: co-retrieval of two traces tightens the edge between them via Hebbian weight updates ("neurons that fire together wire together").
On top of the encoding/decay/retrieval substrate, the runtime ships eight optional neuroscience-grounded mechanisms — reconsolidation, retrieval-induced forgetting, involuntary recall, metacognitive feeling-of-knowing, temporal gist, schema encoding, source-confidence decay, and emotion regulation. All HEXACO-personality-modulated and individually configurable via cognitiveMechanisms on CognitiveMemoryConfig. See the Mechanism Implementation Reference below for hook points, APIs, and testing.
What it actually does, in five lines
- Encoding is shaped by the agent's HEXACO personality traits and current emotional state (PAD model: valence, arousal, dominance)
- Forgetting follows the Ebbinghaus exponential decay curve, with retrieval-induced reinforcement via spaced repetition
- Retrieval combines six weighted signals (strength, embedding similarity, recency, emotional congruence, graph activation, importance) into a composite score
- Working memory enforces Baddeley's slot-based capacity limits (7±2), modulated by traits
- Consolidation runs periodically to prune weak traces, link related traces in the graph, summarize clusters into schemas, resolve contradictions, and reinforce traces due for review
Core encoding, decay and retrieval make no LLM call with the keyword feature detector (retrieval embeds the query). In the Batch-2 layer, the observer and reflector run only when their config carries an llmInvoker. The memory graph is on unless graph.disabled is true, and with a graph (or a consolidation config) the manager builds the consolidation pipeline and starts its hourly timer unless consolidation.enabled is false. The manager takes its vector store and knowledge graph as interfaces, so the same calls run over any implementation of them.
Cognitive science foundations
Each model below has a one-to-one analogue in the source. The point of the table is not to claim the runtime "uses" these papers in the loose sense — the point is that the constants, formulas, and weights you'll see in the code lines below come straight from this literature.
| Model | Reference | Application in AgentOS |
|---|---|---|
| Multi-store memory | Atkinson & Shiffrin, 1968 | Sensory input → working memory → long-term memory pipeline |
| Working memory model | Baddeley & Hitch, 1974; Baddeley 2003 | Slot-based capacity limits (7±2) with activation levels |
| LTM taxonomy | Tulving, 1972 | Episodic / semantic / procedural / prospective memory types |
| Forgetting curve | Ebbinghaus, 1885 | S(t) = S₀ · e^(-Δt / stability) exponential decay |
| Arousal curve | Yerkes & Dodson, 1908 | Encoding quality peaks at moderate arousal (inverted-U) |
| Flashbulb memories | Brown & Kulik, 1977 | High-emotion events create vivid, persistent traces |
| Mood-congruent encoding | Bower, 1981 | Content matching current mood valence encodes more strongly |
| Spreading activation | Anderson, 1983 (ACT-R) | BFS through associative graph with activation decay |
| Hebbian learning | Hebb, 1949 | Co-retrieval strengthens graph edges |
| HEXACO personality | Ashton & Lee, 2007 | Trait-driven encoding weights and memory capacity modulation |
| Source-monitoring framework | Johnson, Hashtroudi & Lindsay, 1993 | Different memory sources decay at different rates (provenance-aware) |
| HyDE retrieval | Gao et al., 2022 | Generate hypothetical answer, embed that, search for matches |
| GraphRAG | Microsoft Research, 2024 | Entity-graph + community summaries for multi-hop retrieval |
| Generative agents | Park et al., 2023 | Persona + memory + reflection as the long-running agent pattern |
| CoALA framework | Sumers et al., 2023 | Cognitive architectures for language agents — episodic / semantic / procedural memory typology |
Architecture
Per-turn data flow (GMI integration):
User message arrives
1. assembleForPrompt — retrieve() (vector store + 6-signal composite score), then
token-budgeted context assembly → the prompt's retrieved
context, in front of the user's message (first model call)
2. [LLM generates the response; tool rounds run]
3. observe() + encode() — after the turn: the user's message, then the reply
(personality-modulated strength)
Outside the turn:
- checkProspective — time/event/context triggers, for the host to check directly
(assembleForPrompt also checks them each turn)
- runConsolidation — periodic background sweep (timer-based when consolidation is enabled)
Memory Types
Based on Tulving's long-term memory taxonomy with extensions:
| Type | Cognitive Model | AgentOS Usage | Example |
|---|---|---|---|
episodic | Autobiographical events | Conversation events, interactions | "User asked about deployment on Tuesday" |
semantic | General knowledge/facts | Learned facts, preferences, schemas | "User prefers TypeScript over Python" |
procedural | Skills and how-to | Workflows, tool usage patterns | "To deploy, run the deployment pipeline" |
prospective | Future intentions | Goals, reminders, planned actions | "Remind user about the PR review" |
relational | Relationship knowledge | Trust signals, boundaries, emotional bonds | "User asked not to be contacted after 9pm" |
Memory Scopes
Each trace is scoped to control visibility and ownership:
| Scope | Visibility | Persistence | Use Case |
|---|---|---|---|
thread | Single conversation | Conversation lifetime | In-conversation working context |
user | All conversations with a user | Long-term | User preferences, facts, history |
persona | All users of a persona | Long-term | Persona's learned knowledge |
organization | All agents in an org | Long-term | Shared organizational knowledge |
Collections in the vector store are named {prefix}_{scope}_{scopeId} (default prefix: cogmem).
The MemoryTrace Envelope
Every memory is wrapped in a MemoryTrace, the envelope carrying content, provenance, emotional context, and decay parameters:
| Field Group | Key Fields | Purpose |
|---|---|---|
| Identity | id, type, scope, scopeId | Classification and routing |
| Content | content, structuredData, entities, tags | The actual memory data |
| Provenance | sourceType, sourceId, confidence, verificationCount, contradictedBy | Source monitoring to prevent confabulation |
| Emotional Context | valence, arousal, dominance, intensity, gmiMood | PAD snapshot at encoding time |
| Decay Parameters | encodingStrength (S0), stability (tau), importance, retrievalCount, accessCount, lastAccessedAt | Ebbinghaus curve inputs |
| Spaced Repetition | reinforcementInterval, nextReinforcementAt | Interval doubling schedule |
| Graph | associatedTraceIds | Links to related traces |
| Trust | policy | Whether the trace may be used for authorization, personalization or fact claims |
| Lifecycle | createdAt, updatedAt, consolidatedAt, isActive | Timestamps and soft-delete flag |
Source types: user_statement, agent_inference, tool_result, observation, reflection, external, fact_graph, typed_network, retrieved_document, human_approval, identity_provider, system_config, external_api, memory_summary.
Encoding Model
Source: src/cognition/memory/core/encoding/EncodingModel.ts
Encoding decides how hard a new trace gets stamped in. Four cognitive mechanisms compose into one strength score:
1. HEXACO Personality -> Encoding Weights
Each HEXACO trait modulates attention to specific content features:
| Trait | Attention Weight | Formula | Effect |
|---|---|---|---|
| Openness | noveltyAttention | 0.3 + O * 0.7 | High O notices novel, creative content |
| Conscientiousness | proceduralAttention | 0.3 + C * 0.7 | High C notices procedures, structure |
| Emotionality | emotionalSensitivity | 0.2 + E * 0.8 | High E amplifies emotional content |
| Extraversion | socialAttention | 0.2 + X * 0.8 | High X notices social dynamics |
| Agreeableness | cooperativeAttention | 0.2 + A * 0.8 | High A notices cooperation cues |
| Honesty | ethicalAttention | 0.2 + H * 0.8 | High H notices ethical/moral content |
The composite attention multiplier starts at 0.5 and adds, for each detected feature, its weight times 0.15 (novelty, procedure) or 0.10 (emotion, social, cooperation, ethics), plus 0.15 for a contradiction and topicRelevance × 0.15, capped at 1.0.
2. Yerkes-Dodson Arousal Curve
Encoding quality peaks at moderate arousal (inverted U):
f(a) = 1 - 4 * (a - 0.5)^2
where a = arousal normalised to [0, 1]
Returns a multiplier in [0.3, 1.0], peaking at a = 0.5. Very low arousal (bored) and very high arousal (panicked) both impair encoding.
3. Mood-Congruent Encoding
Content whose emotional valence matches the current mood is encoded more strongly:
boost = 1 + max(0, currentValence * contentValence) * emotionalSensitivity * 0.3
Positive product means mood and content are congruent (both positive or both negative).
4. Flashbulb Memories
When emotional intensity exceeds the threshold (default: 0.8), the memory becomes a flashbulb memory:
- Strength multiplier:
2.0x(default) - Stability multiplier:
5.0x(default)
These model the vivid, persistent memories formed during highly emotional events (Brown & Kulik, 1977).
Composite Encoding Strength
S₀ = min(1.0, base * arousalBoost * emotionalBoost * attentionMultiplier * congruenceBoost * flashbulbBoost)
Default base = 0.5. The stability (time constant for decay) is computed as:
stability = baseStabilityMs * (1 + S₀ * 6) * flashbulbStabilityMultiplier
Default baseStabilityMs = 3,600,000 (1 hour). Stronger memories are inherently more stable.
Content Feature Detection
The encoding model needs to know what features the content contains. Three detection strategies are available:
| Strategy | LLM calls | Behavior |
|---|---|---|
keyword | 0 | Keyword rules |
llm | 1 per encode | The model classifies the content; featureDetectionLlmInvoker is required, and the detector throws without it |
hybrid | 0 | Keyword rules on encode; its LLM path (detectWithLlm()) has no caller in the runtime |
Detected features (ContentFeatures): hasNovelty, hasProcedure, hasEmotion, hasSocialContent, hasCooperation, hasEthicalContent, hasContradiction, topicRelevance.
Configure via featureDetectionStrategy in CognitiveMemoryConfig.
Forgetting & Decay
Source: src/cognition/memory/core/decay/DecayModel.ts
Ebbinghaus Forgetting Curve
Memory strength decays exponentially over time:
S(t) = S₀ * e^(-dt / stability)
where:
S₀ = initial encoding strength
dt = time elapsed since last access (ms)
stability = time constant (ms); grows with each retrieval
Spaced Repetition
Each successful retrieval updates the trace via the desirable difficulty effect:
- Difficulty bonus:
max(0.1, 1 - currentStrength)— weaker memories get larger stability boosts - Diminishing returns:
1 / (1 + 0.1 * retrievalCount)— logarithmic saturation - Emotional bonus:
1 + intensity * 0.3— emotional memories consolidate faster - Growth factor:
(1.5 + difficultyBonus * 2.0) * diminish * emotionalBonus - Interval doubling:
reinforcementInterval *= 2after each retrieval - Strength:
encodingStrength + 0.1, capped at 1.0
Interference
computeInterference() computes interference between a new trace and existing traces above the similarity threshold (default 0.7): a retroactive strength reduction for each old trace (0.15 at similarity 1.0) and a proactive reduction for the new one (capped at 0.3). The memory manager does not call it, so encoding applies no interference.
Pruning
Traces with currentStrength < pruningThreshold (default: 0.05) are soft-deleted during consolidation, unless their emotional intensity exceeds 0.3 (emotional memories are protected from pruning).
The retention sweeps of MemoryLifecycleManager read traces through a vector store's scanByMetadata(); on a store without it they cannot enumerate traces.
Retrieval Priority Scoring
Source: src/cognition/memory/core/decay/RetrievalPriorityScorer.ts
Retrieval combines six signals into a composite score:
| Signal | Weight | Range | Computation |
|---|---|---|---|
strength | 0.25 | 0-1 | S₀ * e^(-dt / stability) |
similarity | 0.35 | 0-1 | Cosine similarity from vector search |
recency | 0.10 | 0-1 | e^(-elapsed / recencyHalfLifeMs) |
emotionalCongruence | 0.15 | 0-1 | max(0, moodValence * traceValence) |
graphActivation | 0.10 | 0-1 | Spreading activation score (0 without graph) |
importance | 0.05 | 0-1 | confidence * 0.5 + 0.5 |
Composite score:
score = clamp(0, 1,
w_str * strengthScore +
w_sim * similarityScore +
w_rec * recencyNorm +
w_emo * emotionalNorm +
w_graph * graphActivation +
w_imp * importanceScore
)
Setting neutralMood: true in retrieval options disables emotional congruence bias (useful for factual lookups).
Tip-of-the-Tongue Detection
Traces with high vector similarity (>0.6) but low strength (<0.3) or low confidence (<0.4) are returned as PartiallyRetrievedTrace — the agent "almost" remembers them. These include suggestedCues (tags) to help the user provide more context.
Working Memory (Baddeley's Model)
Source: src/cognition/memory/core/working/CognitiveWorkingMemory.ts
Working memory is a slot-based, capacity-limited buffer that tracks what the agent is currently "thinking about."
Capacity
Base capacity follows Miller's number (7), modulated by personality:
- High openness (>0.6): +1 slot (broader attention span)
- High conscientiousness (>0.6): -1 slot (deeper focus per item)
- Result clamped to
[5, 9](Miller's 7 plus/minus 2)
Slot Mechanics
Each WorkingMemorySlot tracks:
| Field | Range | Purpose |
|---|---|---|
activationLevel | 0-1 | How "in focus" this item is |
attentionWeight | 0-1 | Proportional share of attention (normalised) |
rehearsalCount | 0+ | Maintenance rehearsal bumps (+0.15 per rehearse) |
enteredAt | Unix ms | When the trace entered working memory |
Activation Lifecycle
- Focus:
focus(traceId, initialActivation)adds a slot (default activation 0.8). The manager focuses each new trace at its encoding strength and each of the top five retrieved traces at its retrieval score. At capacity, the lowest-activation slot is evicted first. - Rehearsal:
rehearse(slotId)bumps activation by 0.15 (capped at 1.0). - Decay: Each
retrieve()lowers every slot's activation byactivationDecayRate(default 0.1). - Eviction: Slots below
minActivation(default 0.15) are evicted. TheonEvictcallback can encode evicted items back to long-term memory.
Prompt Formatting
formatForPrompt() outputs slots sorted by activation:
- [ACTIVE] mt_1234 (activation: 0.85)
- [fading] mt_1235 (activation: 0.52)
- [weak] mt_1236 (activation: 0.20)
Memory Store
Source: src/cognition/memory/retrieval/store/MemoryStore.ts
The MemoryStore wraps IVectorStore + IKnowledgeGraph into a unified persistence layer:
- Store: Embeds content via
IEmbeddingManager, upserts into vector store, records as episodic memory in knowledge graph - Query: Vector search -> decay-aware scoring -> tip-of-the-tongue detection
- Access tracking: Updates spaced repetition parameters on each retrieval
- Soft delete: Sets
isActive = falseand keeps the Brain row as a tombstone; the trace leaves every vector index and its episodic copy leaves the knowledge graph (Deleting traces)
Access state across restarts
With a Brain attached (CognitiveMemoryConfig.brain), store() writes each trace's row and every access updates it. An access writes last_accessed, retrieval_count and strength, and merges the trace's stability, access count, reinforcement interval and next reinforcement time into the row's metadata JSON under decay, the object the Memory facade, its consolidation and the memory tools read and write; the row's other keys stay. Another writer can change that object while a store holds the trace: the Memory facade's recall or feedback on the same Brain, another store on the same Brain, a consolidation merge. An access that finds the row's decay changed since the store took the trace or last wrote the row counts the store's accesses since then on top of the row's state and its three columns, and the store's copy of the trace takes the result, so the other writer's reinforcement or penalty stays. A change the store made to its copy in memory alone since then (retrieval-induced forgetting, a consolidation's source-confidence decay) gives way to the row's state in that case. A store that loads the row, after a restart or in another process, reads them back, so a trace keeps every reinforcement, and the consolidation's spaced-repetition sweep reinforces the traces whose next reinforcement time has passed. The merge is a compare-and-set on the JSON text the access read. When another writer changes the row in between, the access reads it again, and after a second lost race it writes the three columns alone; a row whose metadata is not a JSON object keeps it and gets the three columns.
With no Brain, the vector store is the store's only record. Each vector document's metadata carries the trace's stability, access count, reinforcement interval and next reinforcement time, and a store started later over the same vector store (a durable index after a restart) restores them with the trace, through getByScope() and through query(). A document that holds no interval restores with one hour, which the trace's next access doubles. With no trace of the scope cached, getByScope() reads the index with a zero vector of the embedding model's size, as the embedding manager reports it (getEmbeddingDimension()) or as one probe embedding measures it, since an index refuses a query vector of another size; the traces restore whatever size the model returns (768 for nomic-embed-text, 1536 for text-embedding-3-small).
persistAccessState: false (on CognitiveMemoryConfig, and memory.persistAccessState on gmi()) restores the earlier behaviour for one release of before-and-after measurement. An access then writes the three columns alone, and a load gives the access count 0, the reinforcement interval one hour and the stability store() wrote; with no Brain, the vector metadata carries no schedule, and a trace restored from it has no next reinforcement time and the interval 0 through getByScope() or one hour through query().
Loading and moving one scope
A store loads the 1,000 most recent traces of its Brain at its first recall, whatever their scope. MemoryStore.hydrateScope(scope, scopeId, limit?) loads one scope's most recent traces by the scope_id column, 1,000 by default, and skips the traces the store holds already, so a returning user's older memories are recallable however many traces other scopes hold. It returns how many traces it loaded, and refuses a limit that is not a positive integer with a RangeError. It reads by the column, so a row a release older than the column wrote since the Brain opened, which holds '' there until the next open files it, is loaded by the whole-brain hydration while it is among the brain's 1,000 most recent.
MemoryStore.refileScope(scope, fromScopeId, toScopeId) moves one scope id's traces to another, as a host does once when it starts passing an organization id: a learner under a namespace reads user:<namespace>/<userId>, not the plain form filed before. Every Brain row filed under the old id moves (scope_id and the metadata's scopeId), soft-deleted rows included, so a later delete of the new id finds them all. Each live trace the store holds moves with its vector document, from the old collection into the new one, with the vector the store holds, the one its Brain row or the index holds, or a new embedding of its text. A trace the store does not hold leaves the old collection, and a store files it in the new one when it loads it (hydrateScope, or the whole-brain hydration). A live trace's knowledge-graph copy moves where the graph holds one; a trace with no copy gets none. With no Brain, the old collection is walked from its start as deleteScope walks it, and the documents another store filed into a durable index before a restart move too; a stale document there, of a trace the store holds under another scope, is deleted from it, as deleteScope deletes one. Each Brain row's metadata is written with a compare against what the move read, so a write another writer makes to the row meanwhile, such as a facade's recall or feedback, is read again and kept. A hydration of this store, or of another store on the same Brain in this process, that read a row before the move reads it again before it loads it, so the trace loads under the new id. It returns the number of traces it moved. An empty scope id on either side is refused with a TypeError, so a caller's missing id never moves the traces filed under no scope id or files a scope's traces under none. Each trace's row moves before its vector document, as a Brain's re-file moves its rows: a store on the same Brain or vector index that reads the new id meanwhile finds no document there yet, where a document whose row still named the old id would read as stale, and such a read deletes a stale document. It throws when a live trace's vector cannot be embedded or written into the new collection, or when a row changes under each of three reads; the traces moved before it stay moved, and a call made again moves the rest. A trace whose vector could not be written has its row under the new id already, and the store reads it under neither id until that call moves its vector. A trace the store holds under the old id whose row another store or process moved to another scope or a third id, or deleted for good, before this move or while it runs, stays as the store holds it, and the move does not count it: the words of a deleted trace go into no collection.
A document the index fails to delete from the old collection stays there, with a warning. Recall under the old id skips it, and a delete of the old id leaves the trace and deletes the document: query(), getByScope() and the collection walk of deleteScope and refileScope read a document as its trace only when the store does not hold the trace under another scope than the document's collection. Another store open on the same Brain keeps what it holds under the old id until it re-files the scope itself or loads it again, and meanwhile none of its reads return the trace under the old id, since the trace's row is filed under the new one (the next section). Its deleteScope of the old id leaves that trace too: the trace is the new id's.
The written form of a plain id, and a Brain's re-file
Where a learning layer exists, a plain id that holds a /, a % or a : is written percent-encoded in every user and persona scope id (encodeURIComponent, with a lone surrogate written %u and four uppercase hexadecimal digits), and every other id keeps its form: team/ana is written team%2Fana, a:b is written a%3Ab, and ana stays ana. A persona id is split at its last ::, and each part is encoded on its own. The rule lives in scopeIdForm.ts (encodeScopeIdPart, encodePlainScopeId); an encoded id holds a % and an unchanged one holds none, so no two ids share a written form.
The first time a layer opens a Brain (CognitiveMemoryManager.ensureEncodedScopeIds, Memory.ensureEncodedScopeIds), refileBrainScopeIds re-files the Brain's plain user and persona scope ids that hold one of the three characters, in one transaction that writes the record brain_meta (brain_id, 'scope_id_form', 'encoded') whatever the Brain holds. Every row moves, its scope_id and its metadata's scopeId together, soft-deleted rows included, and a row a release older than the scope_id column wrote since the Brain opened is filed first, so it moves with the rest. The knowledge-graph memory nodes the Brain holds under such an id (a trace's copy, and a copy whose trace is gone) move in the same transaction, their stored context and their participants, and keep their relations. Each row and node is written with a compare against what the re-file read, so on Postgres a write another process commits meanwhile is read again and kept. On Postgres the transaction also takes an advisory lock on the brain first, so two processes that open one Brain together re-file it once. The SQLite re-files of one process take turns; a second process's transaction on the same file fails, and a later call finds the record. A Brain that holds the record is left as it is. Once the record is written, a scope id that holds a / is a layer's namespaced id: team/ana names user ana of namespace team.
Once the re-file has committed, every live MemoryStore in this process on the same Brain (a handle opened on the same file, the same connection string or the same adapter) moves what it holds of the traces the re-file moved, and of those alone. A trace it holds under the old id moves with its vector document, and its knowledge-graph copy is recorded again under the new id where its graph holds the copy under another (an in-memory or a Neo4j graph); one whose row is gone by then, deleted for good by another store or process once the re-file committed, stays as the store holds it, and none of its words are written. The old collection's document of a moved trace it does not hold goes. A row filed under an old id after the re-file is a namespaced learner's, and stays where it is. A hydration under way loads its rows as the Brain holds them after the re-file. Then ensureEncodedScopeIds follows the Brain's rows, whoever re-filed it (this call, another handle or another process): each trace the store holds under a user or persona id that the encoded form would change moves to the scope id its row holds, and one whose row holds the id as it is stays, since a namespaced id holds a / too. A move the re-file's own pass could not make is made there, and one that fails throws. ensureEncodedScopeIds returns how many rows its own re-file moved: none when the Brain held the record.
Until a store follows a move, its reads skip what the move left behind. query(), getByScope() and filterRecallableTraceIds() check each trace they would return against its Brain row, and a trace whose row is filed under another scope or another scope id than the one the read reaches it under is not returned there; query() and getByScope() delete such a document from the collection when the store does not hold its trace. The same holds after a move by refileScope in another store or another process. A row that names no scope id (a release older than the column wrote it with no scopeId in its metadata) is read as filed where the read reaches it. A trace snapshot read before the re-file (readTraceSnapshot, whose scopeIdForm is 'plain') is restored under the encoded id (Memory.restoreTrace), so a ledger revert never files the plain user's trace under the namespaced id its plain form now names. A delete follows the rows too: deleteScope of an old id deletes none of the traces the store holds under it whose rows moved, so a delete of user ana of namespace team (team/ana) through a store that has not followed the re-file leaves the plain user team/ana's traces, now filed under team%2Fana; and deleteTraces of a trace the store holds under the old id takes its document from the collection its row names as well, where another process's store moved it.
An import keeps a Brain's form, and takes its source's form from the source's own record: a JSON export's meta, the brain_meta of the brain a SQLite file holds, or scopeIdForm: encoded in the front matter of a note an encoded Brain's Markdown export wrote; a source without one, a CSV file included, is plain, and so is a source with the record whose user and persona scope ids hold none of the three characters, since they read the same in both forms: a Brain without the record stays without it, and a layer that opens it later re-files the plain ids filed there meanwhile. A plain source goes into a Brain that holds the record with its user and persona scope ids encoded as they are written, each trace's knowledge-graph copy with them. A source with the record goes into a Brain without it only when that Brain holds no plain id the encoded form changes, in a trace or a copy, and the Brain then gets the record; otherwise the import is refused, naming the record. A Brain that takes the record from an import holds the encoded form from then on, so serve it with a learning layer: a host with no layer files its plain ids as they are, which the record then reads as encoded or namespaced. The import reads the Brain's form in the transaction it writes in, after the brain's advisory lock on Postgres, so it follows a re-file that commits first. An export reads its record and its rows in one form. The RAG MigrationEngine copies rows as they are, so it refuses, naming the record, where an import would encode. A Qdrant source or target takes part through its SQLite sidecar's brain_meta: its traces are in Qdrant, where the handshake reads no scope id, so a brain the sidecar holds counts as holding ids the two forms read apart. A source that names no brain (a Pinecone index, a Qdrant source with no sidecar) is refused into a target that holds the encoded form and traces, and a Qdrant target with no sidecar takes no source brain that holds the record. Import and Export gives each format's rule.
Two limits hold. A knowledge graph outside the Brain (Neo4j) keeps under the plain id the copies of the traces no store in the process held, and the copies whose trace is gone; a delete of a trace by its id still takes its copy. Two handles on one SQLite file in one process are two connections: a re-file that meets a write transaction of the other handle waits for it with the event loop stopped, for the driver's busy timeout of 5 seconds, and fails when the other outlasts it; a later call re-files the Brain.
The re-file reads the Brain alone. A store with no Brain has no record to write and is not re-filed, since a vector index cannot list its collections: a durable index that holds documents filed under such a plain id before a layer existed keeps them under it, where the namespaced id of the same text (team/ana) reads them, so a host that keeps such ids in a durable index with no Brain moves each one to its encoded form with refileScope, which walks the old collection, before a layer serves the index. A namespaced id filed in a Brain that holds no record yet reads to the re-file as a plain one, and the re-file encodes it: refileScope('user', 'ana', 'team/ana') before the re-file leaves user ana's traces under team%2Fana, which a learner of namespace team does not read. A host re-files the Brain (ensureEncodedScopeIds) before it moves a user's traces under a namespace.
Deleting traces
store() records each trace in the knowledge graph as an episodic memory holding the trace's full text, with a context that names the trace (memoryTraceId), its scope and its scope id. A soft delete (MemoryStore.softDelete: a superseded trace, a pruned one, the loser of a conflict) marks the Brain row deleted = 1 and keeps it as a tombstone, evicts the trace from every vector index on the same backing store, and deletes its episodic copy (IKnowledgeGraph.deleteMemories). The facade's soft deletes do the same on a Brain a manager files into: Memory.forget() and the memory tools' memory_delete and memory_merge delete each deleted trace's copy (Memory.deleteGraphCopies). The in-memory, SQL and Neo4j knowledge graphs implement deleteMemories; a graph without it keeps its copies. Each of the three keeps one copy per trace and finds it with no scan: the in-memory and SQL graphs key the copy by its trace (node id trace:<traceId>) and the Neo4j graph writes the trace id on it (traceId, indexed), so recording a trace again replaces its copy. A copy an older release recorded has a random node id (SQL) or no traceId (Neo4j); a graph instance's first delete by trace id gives such copies their key or their trace id. In SQL the copy is written under its key and the relations that name it move with it before its older node goes, so a move that fails, which the graph warns of, leaves the copy for the next instance, and a delete by its trace id in the meantime deletes it where it is. A pass that fails as a whole (the SQL graph's read of such copies, a Neo4j read or write), which the graph warns of, leaves each delete by trace id reading the trace's older copies by the text their stored context holds ("memoryTraceId":"<id>", matched exactly in code), and the next delete by trace id makes the pass again. An import into a Brain (SqliteImporter, Brain.importFromSqlite, a JSON import) files each copy it writes under its key, with the relations that name it, and deletes one whose key the brain's own copy of the trace holds, so a delete by trace id finds what it brings in. SqliteImporter and a JSON import bring a trace's copy in with the trace alone: a trace they merge into one the brain holds as a duplicate, or write under another id because its id names another trace, comes with no copy and none of the copy's relations, so the delete of the trace the brain holds leaves none of its words. A copy a release older than keyed copies writes into the same database after that, while both releases run on it, gets its key or trace id from the next instance, and a delete by its scope finds it at any time. A copy's relations (a host can relate one, upsertRelation) are deleted with it. The SQL graph matches a scope id by its text in the stored JSON with an escaped LIKE, so a scope id that holds a quote, a backslash, % or _ finds its own copies on SQLite and Postgres alike. The SQL graph keeps each brain's copies apart by brain id; the Neo4j graph files no brain id on a copy, so one Neo4j database holds one agent's graph, and a delete by scope there takes every copy filed under that scope and scope id, whichever agent recorded it. With enableGraphActivation, the entity nodes a trace named stay, with the co-occurrence edges between them: an entity node is shared by every trace that names it.
CognitiveMemoryManager.deleteTraces(traceIds) and deleteScope(scope, scopeId) delete for good. For every trace the store finds (a Brain row, or a trace the store holds), each first removes what the manager keeps of it: the memory graph's node, with the node's edges; its original in the configured archive (archive, through IMemoryArchive.drop, which takes its access log too), so rehydrate() finds nothing; and the typed facts the manager extracted from it at encode (typedNetwork.extractAtEncode, the facts <traceId>-fact-<n>, with their edges). It then deletes the traces' knowledge-graph copies, which hold their full text: a graph that cannot delete them fails the call before any trace is deleted, with every row kept, so a later call finds the same traces. It then soft-deletes each trace and deletes its Brain row, so no store, in this process or another, loads it again, and every store on the same Brain or vector index in the process keeps of it only where it was filed, none of its words (getTrace and listTraces give its id and scope with no content). What the manager keeps goes first, so a call that fails can be made again and finds the same traces. An id the store finds no trace of is skipped: nothing the manager keeps under that id is touched, a knowledge-graph entity of that id included. The rows that name a trace go before its row, as the Brain's foreign keys require: its retrieval_feedback rows, which hold the query that recalled it, are deleted, and a document_chunks row the trace was filed from stops naming it and keeps its text, which belongs to its document. deleteScope takes every trace of the scope the store holds and every Brain row filed under the scope (memory_traces.scope_id), soft-deleted rows and another process's included, then every episodic copy filed under the scope, a copy whose trace is already gone included: a graph that cannot delete those fails the call, once the scope's traces are deleted, so a second call deletes the copies. A trace the store holds under the scope whose row a move filed under another scope id (refileScope in another store or process, a Brain's re-file the store has not followed) stays: it belongs to that scope id. A trace deleted for good that the store holds under another scope than its row names leaves the collection its row names too, where the store that moved it filed its document. Before it reads the rows it files the brain's rows that hold scope_id '' while their metadata names a scope id, as an open and an import do: a release older than the column writes such rows to a brain this process holds open. On SQLite the full-text index is rebuilt from the rows that remain; a rebuild that fails is repaired when the Brain next opens. Both return { deleted, graph }: deleted counts the traces that had a Brain row or were live in the store (with no Brain, a live document of the vector index counts), so a call made again counts none, and graph is 'not_supported' when the knowledge graph has no deleteMemories.
A trace a second user filed on another user's session carries that user's id, MemoryTrace.turnUserId (set through encode's turnUserId), in its Brain row's metadata and, with no Brain, in its vector document's metadata. deleteScope(scope, scopeId, { turnUserId }) deletes only the traces of the scope that user filed, with their memory-graph nodes and episodic copies, and leaves the scope's other traces and copies. An empty turnUserId is refused with a TypeError, so a caller's missing id never widens the delete to the whole scope, and so is an empty scope id, so it never deletes every trace filed under no scope id.
With no Brain, the vector index is the scope's record, and a store started over a durable index caches none of what another store filed into it before a restart. deleteScope then walks the scope's collection from its start, 500 documents a read, and deletes each page's traces with their memory-graph nodes and episodic copies. It reads through the index's metadata scan (scanByMetadata), following the cursor the scan returns, or, for an index without one, through similarity queries with a zero vector of the embedding model's size, which an approximate index answers with the documents its search reaches. It walks the collection again while a walk finds a document the call has not read, since a delete moves the documents after it past a cursor that counts positions. A document whose eviction fails stays in the index, with a warning, and the next call reads it again. A document of a trace the store holds under another scope (stored again under another scope id, or moved by refileScope when its delete from the old collection failed) is a stale copy of the trace's words: deleteScope deletes it from the collection and keeps the trace (with turnUserId, only a copy of that user's trace), and neither getByScope() nor query() returns the trace for the scope whose collection holds the copy. The store's cache decides which copy is current: a store started over a durable index that first reads a trace from its stale copy holds the trace under that copy's scope until it stores, loads or moves the trace again. With no Brain, deleteTraces reaches the traces the store holds: an id alone names no collection, so a trace another store filed into the index before a restart is deleted through deleteScope. An index that reports the collection missing (collectionExists) holds nothing of the scope; a read that fails on an index holding the collection fails the call, and on an index that cannot say whether it holds it, the read counts as finding nothing, with a warning.
Collection Naming
Collections follow the pattern {prefix}_{scope}_{scopeId}:
cogmem_user_agent-123
cogmem_thread_conv-456
cogmem_persona_helper-bot
cogmem_organization_acme-org
Memory Graph
Source: src/cognition/memory/retrieval/graph/IMemoryGraph.ts
The IMemoryGraph interface abstracts over two backends:
| Backend | Implementation | Use Case |
|---|---|---|
graphology | GraphologyMemoryGraph | Dev/testing, in-memory, fast |
knowledge-graph | KnowledgeGraphMemoryGraph | Production, wraps IKnowledgeGraph |
Configure via graph.backend (default: 'knowledge-graph').
Edge Types
| Edge Type | Meaning | Weight |
|---|---|---|
SHARED_ENTITY | Traces mention the same entity | 0.5 |
TEMPORAL_SEQUENCE | Traces created within 5 minutes | 0.3 |
SAME_TOPIC | Traces share topic cluster | varies |
CONTRADICTS | Traces contain conflicting information | varies |
SUPERSEDES | One trace replaces another | varies |
CAUSED_BY | Causal relationship | varies |
CO_ACTIVATED | Traces retrieved together (Hebbian) | grows |
SCHEMA_INSTANCE | Episodic trace is instance of semantic schema | 0.6 |
Consolidation creates the SHARED_ENTITY, TEMPORAL_SEQUENCE and SCHEMA_INSTANCE edges; recordCoActivation() creates and strengthens CO_ACTIVATED edges.
Spreading Activation
Source: src/cognition/memory/retrieval/graph/SpreadingActivation.ts
Implements Anderson's ACT-R spreading activation model. Given seed nodes (top retrieval results), activation spreads through the graph to surface associated memories.
Algorithm (BFS)
- Seed nodes start at
activation = 1.0 - Each hop:
neighbor_activation = current * edge_weight * decayPerHop - Multi-path summation (capped at 1.0) — traces reachable by multiple paths get boosted
- BFS with
maxDepth(default 3) andactivationThreshold(default 0.1) cutoffs - Results sorted by activation descending, capped at
maxResults(default 20)
Configuration
| Parameter | Default | Effect |
|---|---|---|
maxDepth | 3 | Maximum hops from seed nodes |
decayPerHop | 0.5 | Activation multiplier per hop |
activationThreshold | 0.1 | Minimum activation to continue |
maxResults | 20 | Maximum activated nodes returned |
Hebbian Learning
After retrieval, the top five retrieved memories are recorded via recordCoActivation(). This strengthens CO_ACTIVATED edges between memories that are frequently retrieved together, implementing the Hebbian rule: "neurons that fire together wire together."
The learning rate (default 0.1) controls how quickly edge weights grow.
Observer/Reflector System
Memory Observer
Source: src/cognition/memory/pipeline/observation/MemoryObserver.ts
The observer monitors accumulated conversation tokens via a buffer. When the threshold is reached (default: 30,000 tokens), it extracts concise observation notes via a persona-configured LLM.
Personality bias in observation:
| High Trait | Observer Focus |
|---|---|
| Emotionality | Emotional shifts, tone changes, sentiment transitions |
| Conscientiousness | Commitments, deadlines, action items, structured plans |
| Openness | Creative tangents, novel ideas, exploratory topics |
| Agreeableness | User preferences, rapport cues, communication style |
| Honesty | Corrections, retractions, contradictions |
Observation notes are typed: factual, emotional, commitment, preference, creative, correction.
Memory Reflector
Source: src/cognition/memory/pipeline/observation/MemoryReflector.ts
The reflector consolidates accumulated observation notes into long-term memory traces. Activates when note tokens exceed threshold (default: 40,000 tokens).
Pipeline (the reflector's instructions to its model):
- Merge redundant observations
- Elevate important facts to long-term traces
- Detect conflicts against existing memories
- Resolve conflicts based on personality:
- High honesty: prefer newer information, supersede old
- High agreeableness: keep both versions, note discrepancy
- Default: prefer higher confidence
Target compression: 5-40x (many observations -> few high-quality traces).
Personality also controls memory style:
- High conscientiousness: structured, well-organized traces
- High openness: rich, associative traces with connections
- Default: concise, factual traces
Prospective Memory
Source: src/cognition/memory/retrieval/prospective/ProspectiveMemoryManager.ts
Prospective memory handles future intentions — "remember to do X when Y happens."
Trigger Types
| Type | Fires When | Example |
|---|---|---|
time_based | Current time >= triggerAt | "Remind me at 3pm" |
event_based | Named event in context.events | "When user mentions deployment" |
context_based | Query embedding similarity > threshold | "When we discuss pricing" |
Registration
await manager.register({
content: 'Remind user about the PR review',
triggerType: 'time_based',
triggerAt: Date.now() + 3_600_000, // 1 hour
importance: 0.8,
recurring: false,
});
Checking
assembleForPrompt() checks the items each turn and puts the triggered ones in the "Reminders" section of the assembled memory context. Commitment notes from the observer (importance 0.5 or more) and future-intent preference notes are registered as items automatically. Items can be recurring (re-trigger) or one-shot (marked triggered after firing).
Context-based triggers use cosine similarity between the cue embedding and the current query embedding, with a configurable similarityThreshold (default 0.7).
Consolidation Pipeline
Source: src/cognition/memory/pipeline/consolidation/ConsolidationPipeline.ts
Runs periodically (default: every hour) to maintain memory health. Five steps:
Step 1: Decay Sweep
Apply Ebbinghaus curve to all traces, soft-delete those below pruningThreshold (default 0.05). Emotional memories (intensity > 0.3) are protected.
Step 2: Co-Activation Replay
Process recent traces (last 24 hours) to create graph edges:
- SHARED_ENTITY: Traces mentioning the same entity get connected (weight 0.5)
- TEMPORAL_SEQUENCE: Traces created within 5 minutes get connected (weight 0.3)
Step 3: Schema Integration
Use detectClusters() on the memory graph (minimum cluster size: 5). For each cluster, invoke an LLM to summarize member traces into a single semantic knowledge node. Connect via SCHEMA_INSTANCE edges.
Step 4: Conflict Resolution
Scan CONTRADICTS edges and resolve based on personality:
- High honesty (>0.6): Prefer newer information, soft-delete the older trace
- Default: Prefer higher confidence (only if confidence difference >0.2)
Step 5: Spaced Repetition
Find traces past their nextReinforcementAt timestamp and boost them via recordAccess(), which increases stability and doubles the reinforcement interval.
Result
interface ConsolidationResult {
prunedCount: number; // Traces soft-deleted
edgesCreated: number; // Graph edges created
schemasCreated: number; // Semantic schemas from clusters
conflictsResolved: number; // Contradictions resolved
reinforcedCount: number; // Traces reinforced
totalProcessed: number; // Total traces examined
durationMs: number; // Cycle duration
}
Prompt Assembly
Source: src/cognition/memory/core/prompt/MemoryPromptAssembler.ts
Assembles memory context into a single formatted string within a token budget, split across seven sections with overflow redistribution.
Default Budget Allocation
| Section | Budget % | Content |
|---|---|---|
| Persistent Memory | 5% | The persistent markdown working memory, truncated to fit |
| Working Memory | 15% | Active context from slot buffer |
| Semantic Recall | 40% | Retrieved traces other than episodic |
| Recent Episodic | 25% | Retrieved episodic traces |
| Prospective Alerts | 5% | Triggered reminders (Batch 2) |
| Graph Associations | 5% | Spreading activation context (Batch 2) |
| Observation Notes | 5% | Recent observer notes (Batch 2) |
Overflow Redistribution
The budget working memory leaves unused flows to Semantic Recall, and so do the budgets of empty Batch 2 sections (no prospective alerts, no graph context, no observation notes).
Personality -> Formatting Style
The assembler selects a formatting style by comparing conscientiousness, openness and emotionality (ties go in that order):
| Dominant Trait | Style | Output |
|---|---|---|
| Conscientiousness | structured | Bullet points, categories |
| Openness | narrative | Flowing prose, connections |
| Emotionality | emotional | Emphasis on feelings, tone |
Output Sections
## How To Use Your Memories
...
## Persistent Memory
...
## Active Context (in focus — reference directly)
- [ACTIVE] mt_1234 (activation: 0.85)
## Relevant Memories (facts — use as background truth, don't announce)
...
## Recent Experiences (events — weave in naturally, never list)
...
## Reminders (act on these — bring up naturally)
- [time_based] PR review is due
## Related Context (connected memories — use for depth)
...
## Observations
...
## Something This Reminds Me Of
[spontaneous memory] ...
The preamble appears when the budget has room for it and 100 more tokens. assembleForPrompt() never fills the last two sections: it passes no observer notes (their 5% flows to Semantic Recall), and it passes neither the mechanisms engine nor the trace pool that the involuntary-recall section needs. Both sections appear only when a host calls assembleMemoryContext() (from @framers/agentos/cognition/memory) itself, with observationNotes, or with mechanismsEngine and allTraces. How each trace line reads depends on the formatting style.
Token estimation uses ~4 chars per token heuristic.
Infinite Context Window
With infiniteContext.enabled and a maxContextTokens, the manager keeps the conversation inside the model's window: trackMessage() records each message, and compactIfNeeded(systemPromptTokens, memoryBudgetTokens), called before the prompt is assembled, summarizes the oldest messages once the system prompt, the memory budget and the conversation reach compactionThreshold of the window (0.75 by default). It returns the messages to send: one summary message in place of the summarized ones, then the rest as they were.
await memory.initialize({
...deps,
maxContextTokens: 128_000,
infiniteContext: { enabled: true, strategy: 'sliding', preserveRecentTurns: 20 },
});
memory.trackMessage('user', text);
const messages = await memory.compactIfNeeded(systemPromptTokens, memoryBudgetTokens);
const history = memory.getSummaryContext(); // the summaries, oldest first
- Turns. Each user message opens the next turn, and the replies and tool messages after it share its number; messages before the first user message are turn 0.
- What is kept. The newest
preserveRecentTurnsturns (two messages a turn) stay as they are while older messages remain to summarize. A shorter conversation over the threshold gives up its two oldest messages. The newest message, the one the next call answers, is never summarized: when it is all that is left, nothing is compacted and no compaction is logged. - Strategies.
sliding(the default) writes one summary of the oldest messages.hierarchicalkeeps a summary per segment and, with a summarizer, merges six of them into a level-1 summary and four level-1 summaries into a level-2 one; its summary message holds the newest summary whole and the older ones, cut from their end, only as far as the window's room under the compaction threshold allows, so without a summarizer, which merges nothing, they do not pile up in the window (the summary chain keeps them all).hybridalso runs the observer, when one is configured, over the segment and the reflector over its notes, and hands the resulting traces toencode(); their ids go into the compaction's log entry. - Summarizer.
infiniteContext.llmInvokertakes a prompt and answers the summary; without it the manager uses the reflector's, the observer's or the feature-detection invoker, with the system promptYou are a conversation summarizer.With none of them, or when a call fails or answers nothing, compaction writes an extractive summary from the conversation itself: the segment's first and last user messages (sliding), its user messages cut to 80 characters (hierarchical), or the ten most important observation notes, else its first three user messages (hybrid). Inslidingandhybridthe summary an earlier compaction left in the segment comes first, carried forward, so the window keeps the condensed history from the start. The strategy's own extract is cut to the tokens the segment held less the summary message's header (25 at the least), and the earlier summary is cut from its end to what is left of the window's room under the compaction threshold once the system prompt, the memory budget, the kept turns, the header and that extract are counted; each cut falls at a space when one lies in the second half of the length. So the summary takes no more of the window than the turns it replaces, and carried history never grows the window past the threshold. The manager logs one line when it runs without a summarizer. - Transparency.
getCompactionHistory(),searchCompactionHistory(keyword),getContextWindowStats()andgetContextTransparencyReport()read the compaction log;transparencyLevel'full'keeps each entry's dropped fragments and observation notes,'summary'(the default) drops them, and'silent'keeps no entries. The summaries form a chain (RollingSummaryChain) that merges its oldest summaries once it passesmaxSummaryChainTokens(2,000 by default), with the summarizer or, without one, by joining and cutting them.
Source: pipeline/context/.
Configuration
CognitiveMemoryConfig (Top-Level)
interface CognitiveMemoryConfig {
// --- Required dependencies ---
workingMemory: IWorkingMemory; // Existing AgentOS working memory
knowledgeGraph: IKnowledgeGraph; // Existing AgentOS knowledge graph
vectorStore: IVectorStore; // Vector store for embeddings
embeddingManager: IEmbeddingManager; // Embedding generation
// --- Agent identity ---
agentId: string;
traits: HexacoTraits; // { honesty, emotionality, extraversion, agreeableness, conscientiousness, openness }
moodProvider: () => PADState; // Callback to get current mood
// --- Feature detection ---
featureDetectionStrategy: 'keyword' | 'llm' | 'hybrid';
featureDetectionLlmInvoker?: (systemPrompt: string, userPrompt: string) => Promise<string>;
// --- Tuning ---
encoding?: Partial<EncodingConfig>; // See defaults below
decay?: Partial<DecayConfig>; // See defaults below
workingMemoryCapacity?: number; // Default: 7 (Miller's number)
tokenBudget?: Partial<MemoryBudgetAllocation>;
collectionPrefix?: string; // Default: 'cogmem'
// --- Batch 2 (optional, no-op when absent) ---
observer?: Partial<ObserverConfig>;
reflector?: Partial<ReflectorConfig>;
graph?: Partial<MemoryGraphConfig>;
consolidation?: Partial<ConsolidationConfig>;
// --- Further options ---
persistentMemory?: PersistentMemorySource; // the persistent markdown working memory
cognitiveMechanisms?: CognitiveMechanismsConfig;
infiniteContext?: Partial<InfiniteContextConfig>;
maxContextTokens?: number; // required for infiniteContext
enableGraphActivation?: boolean;
brain?: Brain;
persistAccessState?: boolean; // Default: true (see Access state across restarts)
}
Encoding Defaults
| Parameter | Default | Description |
|---|---|---|
baseStrength | 0.5 | Base encoding strength before modulation |
flashbulbThreshold | 0.8 | Emotional intensity threshold for flashbulb |
flashbulbStrengthMultiplier | 2.0 | Strength boost for flashbulb memories |
flashbulbStabilityMultiplier | 5.0 | Stability boost for flashbulb memories |
baseStabilityMs | 3,600,000 | Base stability (1 hour) |
Decay Defaults
| Parameter | Default | Description |
|---|---|---|
pruningThreshold | 0.05 | Strength below which traces are pruned |
recencyHalfLifeMs | 86,400,000 | Recency boost half-life (24 hours) |
interferenceThreshold | 0.7 | Cosine similarity threshold for interference |
Graph Defaults
| Parameter | Default | Description |
|---|---|---|
backend | 'knowledge-graph' | Graph backend selection |
maxDepth | 3 | Spreading activation max hops |
decayPerHop | 0.5 | Activation decay per hop |
activationThreshold | 0.1 | Minimum activation to continue |
hebbianLearningRate | 0.1 | Co-activation edge strengthening rate |
Consolidation Defaults
| Parameter | Default | Description |
|---|---|---|
intervalMs | 3,600,000 | Run interval (1 hour) |
maxTracesPerCycle | 500 | Max traces per cycle |
mergeSimilarityThreshold | 0.92 | Similarity threshold for merging |
minClusterSize | 5 | Min cluster size for schema integration |
Quick Start
Minimal setup with core features (no LLM calls, no Batch 2):
import { CognitiveMemoryManager } from '@framers/agentos/memory';
const memory = new CognitiveMemoryManager();
await memory.initialize({
workingMemory: existingWorkingMemory,
knowledgeGraph: existingKnowledgeGraph,
vectorStore: existingVectorStore,
embeddingManager: existingEmbeddingManager,
agentId: 'my-agent',
traits: { openness: 0.7, conscientiousness: 0.8, emotionality: 0.5 },
moodProvider: () => ({ valence: 0, arousal: 0.3, dominance: 0 }),
featureDetectionStrategy: 'keyword',
});
// Encode a user message
const mood = { valence: 0.2, arousal: 0.4, dominance: 0 };
const trace = await memory.encode(
'I prefer deploying with Docker Compose',
mood,
'content',
{ type: 'semantic', scope: 'user', tags: ['deployment', 'docker'] },
);
// Retrieve relevant memories before prompt construction
const result = await memory.retrieve('How should I deploy?', mood, { topK: 5 });
// Assemble for prompt injection (1000 token budget)
const context = await memory.assembleForPrompt('How should I deploy?', 1000, mood);
console.log(context.contextText); // Formatted memory context
console.log(context.tokensUsed); // Actual tokens used
Full setup with all Batch 2 features:
const llmInvoker = async (system: string, user: string) => {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
});
return response.choices[0].message.content ?? '';
};
await memory.initialize({
// ... core config as above ...
observer: { activationThresholdTokens: 30_000, llmInvoker },
reflector: { activationThresholdTokens: 40_000, llmInvoker },
graph: { backend: 'knowledge-graph', maxDepth: 3, decayPerHop: 0.5 },
consolidation: { intervalMs: 3_600_000, minClusterSize: 5 },
});
// Observer: feed each message
await memory.observe('user', 'I need to deploy by Friday', mood);
await memory.observe('assistant', 'I can help with that deployment.', mood);
// Prospective: register a reminder
const pm = memory.getProspective()!; // built by initialize()
await pm.register({
content: 'User needs deployment done by Friday',
triggerType: 'time_based',
triggerAt: fridayTimestamp,
importance: 0.9,
recurring: false,
});
// Consolidation runs automatically on timer, or manually:
const consolidation = await memory.runConsolidation();
console.log(`Pruned ${consolidation.prunedCount}, created ${consolidation.schemasCreated} schemas`);
Integration with GMI
A GMI with cognitive memory calls the manager through CognitiveMemoryBridge at two points of its turn. The calls below are the bridge's, in outline; mood is the GMI's PAD state and gmiMood its mood label.
Before Prompt Construction (Retrieve + Assemble)
// Before the turn's first model call, with the user's message as the query:
const memoryContext = await cognitiveMemory.assembleForPrompt(
userMessage,
1600, // token budget
mood,
{ scopes }, // the scopes of the turn's user, session, conversation, persona and organization
);
// memoryContext.contextText joins the prompt's retrieved context
After the Turn (Observe + Encode)
// After the turn, for the user's message:
await cognitiveMemory.observe('user', userMessage, mood);
await cognitiveMemory.encode(userMessage, mood, gmiMood, {
type: 'episodic',
scope: 'user',
scopeId: userId,
sourceType: 'user_statement',
});
// Then for the reply:
await cognitiveMemory.observe('assistant', assistantResponse, mood);
await cognitiveMemory.encode(assistantResponse, mood, gmiMood, {
type: 'semantic',
scope: 'user',
scopeId: sessionId,
sourceType: 'agent_inference',
});
With a learning layer the GMI reads and files under the scope ids its learner session gives the turn: the user's message and the reply both go under the turn's own user scope id (the learner's, or the session's form on a turn with no learner), each encode runs inside the user's write chain with the tombstone read before it and once it has committed, and the recall lists only the turn's own scopes (Where a turn's memory is filed and read).
What the layer adds to a vector store
| # | Plain vector memory | Cognitive memory |
|---|---|---|
| 1 | Every memory equally strong | Encoding strength from HEXACO traits, arousal (Yerkes-Dodson) and mood |
| 2 | No forgetting | Ebbinghaus decay with per-trace stability |
| 3 | No spaced repetition | Retrieval grows stability (desirable difficulty) and doubles the review interval |
| 4 | No working memory limit | Slot-based working memory, 5 to 9 slots by traits |
| 5 | No emotional context | PAD snapshot at encoding, mood-congruent retrieval bias |
| 6 | One retrieval signal (similarity) | Six-signal score (strength, similarity, recency, emotion, graph, importance) |
| 7 | No memory graph | IMemoryGraph with 8 edge types and spreading activation |
| 8 | No consolidation | Five steps: decay sweep, replay, schema integration, conflict resolution, reinforcement |
| 9 | No prospective memory | Time, event and context triggers, one-shot or recurring |
| 10 | No observer or reflector | Personality-biased observation notes, reflected into traces by an LLM |
| 11 | No provenance | Source type, confidence, verification count and contradictions on each trace |
Source Files
All source lives in src/cognition/memory/:
| File | Export |
|---|---|
core/types.ts | MemoryTrace, MemoryType, MemoryScope, ScoredMemoryTrace and the other shared types |
core/config.ts | CognitiveMemoryConfig, EncodingConfig, DecayConfig, defaults |
CognitiveMemoryManager.ts | CognitiveMemoryManager, ICognitiveMemoryManager |
core/encoding/EncodingModel.ts | computeEncodingStrength, yerksDodson, buildEmotionalContext |
core/encoding/ContentFeatureDetector.ts | createFeatureDetector, IContentFeatureDetector |
core/decay/DecayModel.ts | computeCurrentStrength, updateOnRetrieval, computeInterference |
core/decay/RetrievalPriorityScorer.ts | scoreAndRankTraces, detectPartiallyRetrieved |
core/working/CognitiveWorkingMemory.ts | CognitiveWorkingMemory |
retrieval/store/MemoryStore.ts | MemoryStore |
core/prompt/MemoryPromptAssembler.ts | assembleMemoryContext |
core/prompt/MemoryFormatters.ts | formatMemoryTrace, FormattingStyle |
retrieval/graph/IMemoryGraph.ts | IMemoryGraph, MemoryEdgeType, ActivatedNode |
retrieval/graph/SpreadingActivation.ts | spreadActivation |
retrieval/graph/GraphologyMemoryGraph.ts | GraphologyMemoryGraph |
retrieval/graph/KnowledgeGraphMemoryGraph.ts | KnowledgeGraphMemoryGraph |
pipeline/observation/MemoryObserver.ts | MemoryObserver, ObservationNote |
pipeline/observation/MemoryReflector.ts | MemoryReflector, MemoryReflectionResult |
pipeline/observation/ObservationBuffer.ts | ObservationBuffer |
retrieval/prospective/ProspectiveMemoryManager.ts | ProspectiveMemoryManager, ProspectiveMemoryItem |
pipeline/consolidation/ConsolidationPipeline.ts | ConsolidationPipeline, ConsolidationResult |
Relationship to Persistent Working Memory
AgentOS provides two complementary working memory systems:
| Baddeley Cognitive Working Memory | Persistent Markdown Working Memory | |
|---|---|---|
| Purpose | In-session attention modeling | Cross-session user context |
| Lifespan | Single session (in-memory) | Persists in a markdown file the host names (Wunderland: agents/<agent id>/working-memory.md in its workspace) |
| Updates | Automatic activation decay | Agent calls update_working_memory tool |
| Format | Capacity-limited slots (7±2) | Free-form markdown template |
| Budget | 15% of prompt tokens | 5% of prompt tokens |
Both are injected into the system prompt simultaneously. The persistent memory appears as ## Persistent Memory before the cognitive slots. See Persistent Working Memory for details.
Mechanism Implementation Reference
The eight cognitive mechanisms live under src/cognition/memory/mechanisms/. Each mechanism is a pure function with one mutation responsibility on a MemoryTrace. The CognitiveMechanismsEngine binds them to lifecycle hooks on MemoryStore and MemoryPromptAssembler.
Source-tree layout
src/cognition/memory/mechanisms/
├── types.ts # CognitiveMechanismsConfig + shared types
├── defaults.ts # DEFAULT_MECHANISMS_CONFIG + resolveConfig()
├── CognitiveMechanismsEngine.ts # Lifecycle hook orchestrator
├── PersonaDriftMechanism.ts # Trait drift proposals during consolidation
├── retrieval/
│ ├── Reconsolidation.ts # Emotional drift on access
│ ├── RetrievalInducedForgetting.ts # Competitor suppression
│ ├── InvoluntaryRecall.ts # Random memory surfacing
│ └── MetacognitiveFOK.ts # Feeling-of-knowing scoring
├── consolidation/
│ ├── TemporalGist.ts # Verbatim→gist compression