Skip to main content

Cognitive Memory

Memory benchmarks (full N=500, gpt-4o and gpt-5-mini readers routed per question type): 85.6% on LongMemEval-S at $0.0090 per correct; with gpt-4o reading 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

See also

HEXACO Personality for the trait-by-trait reference covering encoding weights, working-memory capacity, prompt formatting, observer/reflector bias, and runtime self-modification.

CognitiveMemoryManager architecture: orchestrator dispatches to 8 subsystems, each backed by its substrate


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").

Eight cognitive mechanisms layered on top

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.

ModelReferenceApplication in AgentOS
Multi-store memoryAtkinson & Shiffrin, 1968Sensory input → working memory → long-term memory pipeline
Working memory modelBaddeley & Hitch, 1974; Baddeley 2003Slot-based capacity limits (7±2) with activation levels
LTM taxonomyTulving, 1972Episodic / semantic / procedural / prospective memory types
Forgetting curveEbbinghaus, 1885S(t) = S₀ · e^(-Δt / stability) exponential decay
Arousal curveYerkes & Dodson, 1908Encoding quality peaks at moderate arousal (inverted-U)
Flashbulb memoriesBrown & Kulik, 1977High-emotion events create vivid, persistent traces
Mood-congruent encodingBower, 1981Content matching current mood valence encodes more strongly
Spreading activationAnderson, 1983 (ACT-R)BFS through associative graph with activation decay
Hebbian learningHebb, 1949Co-retrieval strengthens graph edges
HEXACO personalityAshton & Lee, 2007Trait-driven encoding weights and memory capacity modulation
Source-monitoring frameworkJohnson, Hashtroudi & Lindsay, 1993Different memory sources decay at different rates (provenance-aware)
HyDE retrievalGao et al., 2022Generate hypothetical answer, embed that, search for matches
GraphRAGMicrosoft Research, 2024Entity-graph + community summaries for multi-hop retrieval
Generative agentsPark et al., 2023Persona + memory + reflection as the long-running agent pattern
CoALA frameworkSumers et al., 2023Cognitive 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:

TypeCognitive ModelAgentOS UsageExample
episodicAutobiographical eventsConversation events, interactions"User asked about deployment on Tuesday"
semanticGeneral knowledge/factsLearned facts, preferences, schemas"User prefers TypeScript over Python"
proceduralSkills and how-toWorkflows, tool usage patterns"To deploy, run the deployment pipeline"
prospectiveFuture intentionsGoals, reminders, planned actions"Remind user about the PR review"
relationalRelationship knowledgeTrust signals, boundaries, emotional bonds"User asked not to be contacted after 9pm"

Memory Scopes​

Each trace is scoped to control visibility and ownership:

ScopeVisibilityPersistenceUse Case
threadSingle conversationConversation lifetimeIn-conversation working context
userAll conversations with a userLong-termUser preferences, facts, history
personaAll users of a personaLong-termPersona's learned knowledge
organizationAll agents in an orgLong-termShared 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 GroupKey FieldsPurpose
Identityid, type, scope, scopeIdClassification and routing
Contentcontent, structuredData, entities, tagsThe actual memory data
ProvenancesourceType, sourceId, confidence, verificationCount, contradictedBySource monitoring to prevent confabulation
Emotional Contextvalence, arousal, dominance, intensity, gmiMoodPAD snapshot at encoding time
Decay ParametersencodingStrength (S0), stability (tau), importance, retrievalCount, accessCount, lastAccessedAtEbbinghaus curve inputs
Spaced RepetitionreinforcementInterval, nextReinforcementAtInterval doubling schedule
GraphassociatedTraceIdsLinks to related traces
TrustpolicyWhether the trace may be used for authorization, personalization or fact claims
LifecyclecreatedAt, updatedAt, consolidatedAt, isActiveTimestamps 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:

TraitAttention WeightFormulaEffect
OpennessnoveltyAttention0.3 + O * 0.7High O notices novel, creative content
ConscientiousnessproceduralAttention0.3 + C * 0.7High C notices procedures, structure
EmotionalityemotionalSensitivity0.2 + E * 0.8High E amplifies emotional content
ExtraversionsocialAttention0.2 + X * 0.8High X notices social dynamics
AgreeablenesscooperativeAttention0.2 + A * 0.8High A notices cooperation cues
HonestyethicalAttention0.2 + H * 0.8High 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:

StrategyLLM callsBehavior
keyword0Keyword rules
llm1 per encodeThe model classifies the content; featureDetectionLlmInvoker is required, and the detector throws without it
hybrid0Keyword 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 *= 2 after 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:

SignalWeightRangeComputation
strength0.250-1S₀ * e^(-dt / stability)
similarity0.350-1Cosine similarity from vector search
recency0.100-1e^(-elapsed / recencyHalfLifeMs)
emotionalCongruence0.150-1max(0, moodValence * traceValence)
graphActivation0.100-1Spreading activation score (0 without graph)
importance0.050-1confidence * 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:

FieldRangePurpose
activationLevel0-1How "in focus" this item is
attentionWeight0-1Proportional share of attention (normalised)
rehearsalCount0+Maintenance rehearsal bumps (+0.15 per rehearse)
enteredAtUnix msWhen the trace entered working memory

Activation Lifecycle​

  1. 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.
  2. Rehearsal: rehearse(slotId) bumps activation by 0.15 (capped at 1.0).
  3. Decay: Each retrieve() lowers every slot's activation by activationDecayRate (default 0.1).
  4. Eviction: Slots below minActivation (default 0.15) are evicted. The onEvict callback 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 = false and 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:

BackendImplementationUse Case
graphologyGraphologyMemoryGraphDev/testing, in-memory, fast
knowledge-graphKnowledgeGraphMemoryGraphProduction, wraps IKnowledgeGraph

Configure via graph.backend (default: 'knowledge-graph').

Edge Types​

Edge TypeMeaningWeight
SHARED_ENTITYTraces mention the same entity0.5
TEMPORAL_SEQUENCETraces created within 5 minutes0.3
SAME_TOPICTraces share topic clustervaries
CONTRADICTSTraces contain conflicting informationvaries
SUPERSEDESOne trace replaces anothervaries
CAUSED_BYCausal relationshipvaries
CO_ACTIVATEDTraces retrieved together (Hebbian)grows
SCHEMA_INSTANCEEpisodic trace is instance of semantic schema0.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)​

  1. Seed nodes start at activation = 1.0
  2. Each hop: neighbor_activation = current * edge_weight * decayPerHop
  3. Multi-path summation (capped at 1.0) — traces reachable by multiple paths get boosted
  4. BFS with maxDepth (default 3) and activationThreshold (default 0.1) cutoffs
  5. Results sorted by activation descending, capped at maxResults (default 20)

Configuration​

ParameterDefaultEffect
maxDepth3Maximum hops from seed nodes
decayPerHop0.5Activation multiplier per hop
activationThreshold0.1Minimum activation to continue
maxResults20Maximum 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 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

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

  1. Merge redundant observations
  2. Elevate important facts to long-term traces
  3. Detect conflicts against existing memories
  4. 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​

TypeFires WhenExample
time_basedCurrent time >= triggerAt"Remind me at 3pm"
event_basedNamed event in context.events"When user mentions deployment"
context_basedQuery 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​

SectionBudget %Content
Persistent Memory5%The persistent markdown working memory, truncated to fit
Working Memory15%Active context from slot buffer
Semantic Recall40%Retrieved traces other than episodic
Recent Episodic25%Retrieved episodic traces
Prospective Alerts5%Triggered reminders (Batch 2)
Graph Associations5%Spreading activation context (Batch 2)
Observation Notes5%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 TraitStyleOutput
ConscientiousnessstructuredBullet points, categories
OpennessnarrativeFlowing prose, connections
EmotionalityemotionalEmphasis 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 preserveRecentTurns turns (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. hierarchical keeps 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). hybrid also runs the observer, when one is configured, over the segment and the reflector over its notes, and hands the resulting traces to encode(); their ids go into the compaction's log entry.
  • Summarizer. infiniteContext.llmInvoker takes 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 prompt You 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). In sliding and hybrid the 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() and getContextTransparencyReport() 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 passes maxSummaryChainTokens (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​

ParameterDefaultDescription
baseStrength0.5Base encoding strength before modulation
flashbulbThreshold0.8Emotional intensity threshold for flashbulb
flashbulbStrengthMultiplier2.0Strength boost for flashbulb memories
flashbulbStabilityMultiplier5.0Stability boost for flashbulb memories
baseStabilityMs3,600,000Base stability (1 hour)

Decay Defaults​

ParameterDefaultDescription
pruningThreshold0.05Strength below which traces are pruned
recencyHalfLifeMs86,400,000Recency boost half-life (24 hours)
interferenceThreshold0.7Cosine similarity threshold for interference

Graph Defaults​

ParameterDefaultDescription
backend'knowledge-graph'Graph backend selection
maxDepth3Spreading activation max hops
decayPerHop0.5Activation decay per hop
activationThreshold0.1Minimum activation to continue
hebbianLearningRate0.1Co-activation edge strengthening rate

Consolidation Defaults​

ParameterDefaultDescription
intervalMs3,600,000Run interval (1 hour)
maxTracesPerCycle500Max traces per cycle
mergeSimilarityThreshold0.92Similarity threshold for merging
minClusterSize5Min 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 memoryCognitive memory
1Every memory equally strongEncoding strength from HEXACO traits, arousal (Yerkes-Dodson) and mood
2No forgettingEbbinghaus decay with per-trace stability
3No spaced repetitionRetrieval grows stability (desirable difficulty) and doubles the review interval
4No working memory limitSlot-based working memory, 5 to 9 slots by traits
5No emotional contextPAD snapshot at encoding, mood-congruent retrieval bias
6One retrieval signal (similarity)Six-signal score (strength, similarity, recency, emotion, graph, importance)
7No memory graphIMemoryGraph with 8 edge types and spreading activation
8No consolidationFive steps: decay sweep, replay, schema integration, conflict resolution, reinforcement
9No prospective memoryTime, event and context triggers, one-shot or recurring
10No observer or reflectorPersonality-biased observation notes, reflected into traces by an LLM
11No provenanceSource type, confidence, verification count and contradictions on each trace

Source Files​

All source lives in src/cognition/memory/:

FileExport
core/types.tsMemoryTrace, MemoryType, MemoryScope, ScoredMemoryTrace and the other shared types
core/config.tsCognitiveMemoryConfig, EncodingConfig, DecayConfig, defaults
CognitiveMemoryManager.tsCognitiveMemoryManager, ICognitiveMemoryManager
core/encoding/EncodingModel.tscomputeEncodingStrength, yerksDodson, buildEmotionalContext
core/encoding/ContentFeatureDetector.tscreateFeatureDetector, IContentFeatureDetector
core/decay/DecayModel.tscomputeCurrentStrength, updateOnRetrieval, computeInterference
core/decay/RetrievalPriorityScorer.tsscoreAndRankTraces, detectPartiallyRetrieved
core/working/CognitiveWorkingMemory.tsCognitiveWorkingMemory
retrieval/store/MemoryStore.tsMemoryStore
core/prompt/MemoryPromptAssembler.tsassembleMemoryContext
core/prompt/MemoryFormatters.tsformatMemoryTrace, FormattingStyle
retrieval/graph/IMemoryGraph.tsIMemoryGraph, MemoryEdgeType, ActivatedNode
retrieval/graph/SpreadingActivation.tsspreadActivation
retrieval/graph/GraphologyMemoryGraph.tsGraphologyMemoryGraph
retrieval/graph/KnowledgeGraphMemoryGraph.tsKnowledgeGraphMemoryGraph
pipeline/observation/MemoryObserver.tsMemoryObserver, ObservationNote
pipeline/observation/MemoryReflector.tsMemoryReflector, MemoryReflectionResult
pipeline/observation/ObservationBuffer.tsObservationBuffer
retrieval/prospective/ProspectiveMemoryManager.tsProspectiveMemoryManager, ProspectiveMemoryItem
pipeline/consolidation/ConsolidationPipeline.tsConsolidationPipeline, ConsolidationResult

Relationship to Persistent Working Memory​

AgentOS provides two complementary working memory systems:

Baddeley Cognitive Working MemoryPersistent Markdown Working Memory
PurposeIn-session attention modelingCross-session user context
LifespanSingle session (in-memory)Persists in a markdown file the host names (Wunderland: agents/<agent id>/working-memory.md in its workspace)
UpdatesAutomatic activation decayAgent calls update_working_memory tool
FormatCapacity-limited slots (7±2)Free-form markdown template
Budget15% of prompt tokens5% 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
│ ├── SchemaEncoding.ts # Schema-congruent detection
│ ├── SourceConfidenceDecay.ts # Source-type decay multipliers
│ └── EmotionRegulation.ts # Reappraisal & suppression
└── index.ts # Barrel exports

Lifecycle hook points​

FileMethodHookWhen
store/MemoryStore.tsrecordAccess()engine.onAccess(trace, mood)After spaced-repetition update
store/MemoryStore.tsquery()engine.onRetrieval(scored, candidates, cutoff, entities)After scoring, before return
prompt/MemoryPromptAssembler.tsassembleMemoryContext()engine.onPromptAssembly(allTraces, retrievedIds)Before final return, when the input carries mechanismsEngine and allTraces; assembleForPrompt() passes neither
CognitiveMemoryManager.tsencode()engine.onEncoding(trace, embedding)After the trace is embedded
CognitiveMemoryManager.tsinitialize()Engine constructionDynamic import when config present

ConsolidationPipeline calls engine.onConsolidation() in each run when the manager was initialized with cognitiveMechanisms.

Per-mechanism API​

Retrieval-time (synchronous):

applyReconsolidation(trace: MemoryTrace, currentMood: PADState, config): void
applyRetrievalInducedForgetting(retrieved, competitors, config): { suppressedIds: string[] }
selectInvoluntaryMemory(allTraces, alreadyRetrievedIds, config): MemoryTrace | null
detectFeelingOfKnowing(scoredCandidates, retrievalCutoff, config, queryEntities): MetacognitiveSignal[]

Consolidation-time (async; LLM gist extraction is opt-in):

applyTemporalGist(traces, config, llmFn?): Promise<number>
applySchemaEncoding(trace, traceEmbedding, clusterCentroids, config): SchemaEncodingResult
applySourceConfidenceDecay(traces, config): number
applyEmotionRegulation(traces, config): number

HEXACO modulation​

CognitiveMechanismsEngine accepts optional HexacoTraits at construction. When provided, mechanism parameters are scaled by personality dimensions before any hook fires:

this.mechanismsEngine = new CognitiveMechanismsEngine(config.cognitiveMechanisms, config.traits);

Modulation runs once via applyPersonalityModulation(). Trait-to-parameter scaling formulas live in CognitiveMechanismsEngine.ts.

Guard conditions​

  • Flashbulb immunity: traces with encodingStrength >= 0.9 are skipped by RIF, temporal gist and emotion regulation. Reconsolidation skips a trace whose encodingStrength is at least immuneAboveImportance, whose default of 9 no strength (0 to 1) reaches, so by default no trace is immune to it.
  • Dead-trace protection: RIF skips traces with encodingStrength < 0.1.
  • Inactive skip: all consolidation mechanisms skip isActive === false traces.
  • Disabled bypass: every mechanism returns immediately when config.enabled === false.

Rehydration​

CognitiveMemoryManager.rehydrate(traceId) returns a trace's archived verbatim content from the manager's archive. The manager passes that archive to neither the temporal-gist mechanism nor its consolidation pipeline, so gisting through the manager keeps no archived copy to rehydrate; the temporal-gist mechanism archives first only when its own config carries an archive. The archive is backed by IMemoryArchive (default SqlStorageMemoryArchive), which uses the same StorageAdapter contract as Brain. Archive tables (archived_traces, archive_access_log) live in the same database when the adapter is shared. MemoryToolsExtension declares an includeRehydrate option and does not read it, so no rehydrate_memory tool is registered.

Perspective encoding​

PerspectiveObserver.rewrite() rewrites an event witnessed by several agents through each witness's HEXACO personality, mood and relationships, and returns one first-person trace per witness; the host encodes each with encode(..., { perspectiveSource }). The observer does not archive the objective event. Perspective-encoded traces have their reconsolidation driftRate halved so retrieval-time drift does not compound the encoding-time shift. The maxDriftPerTrace cap (0.4) still bounds total drift. Gating: only important-tier witnesses with event.importance >= 0.3 (importanceThreshold) and entity overlap receive LLM rewrites; others fall back to objective encoding.

Metadata storage​

Mechanism metadata is stored in trace.structuredData.mechanismMetadata (type MechanismMetadata), avoiding changes to the core MemoryTrace interface. The metadata persists in the vector store's metadata JSON column.

Testing​

Each mechanism is a pure function testable in isolation:

npx vitest run src/cognition/memory/mechanisms/
npx vitest run src/cognition/memory/mechanisms/__tests__/retrieval.test.ts
npx vitest run src/cognition/memory/mechanisms/__tests__/consolidation.test.ts
npx vitest run src/cognition/memory/mechanisms/__tests__/engine.test.ts
npx vitest run src/cognition/memory/mechanisms/__tests__/types.test.ts

References​

The runtime constants, formulas, weights, and design decisions in this page are grounded in the cognitive-science and information-retrieval literature listed below. Citations are inline throughout the doc; this section consolidates them for review and audit.

Cognitive science foundations​

Personality structure​

Retrieval-augmented generation​

Cognitive architectures for language agents​

Benchmarks​

Implementation references​

Source files cited inline: