ADR-0047: Episodic Memory and Engram Model Hierarchy¶
| Field | Value |
|---|---|
| Status | Accepted (Implemented) |
| Date | 2026-09-03 |
| Authors | Spector Maintainers & Architecture Working Group |
| Deciders | Spector Technical Steering Committee (TSC) |
| Supersedes | None |
| Superseded By | ADR-0030 (Engram Header Alignment) |
| Last Verified | 2026-09-16 (Verified against main) |
1. Context¶
This Architectural Decision Record provides the detailed class hierarchy, store model, facade decomposition, and interaction sequences for the engram storage architecture established in ADR-0046.
Architectural Scope¶
Four tier stores. One encoding-header type. Strength in its own region. No second write API. No MemoryEngine. No TraceLog. No TraceKind.
Episodic memory = what happened = conversation episodes
Semantic memory = what is taken true = facts
Procedural memory = how to act = skills
Working memory = workspace = evictable thoughts
The conversation store is EpisodicMemory (today’s EpisodicLogMemory with the “Log” suffix dropped). Append-only is how it is packed, not what it is called.
If facts and skills do not live in that store, a TURN/FACT/SKILL discriminator on the episode file is dead weight. MemoryType is the kind.
Canonical Terminology & Domain Vocabulary¶
| Term | Meaning | Not |
|---|---|---|
| Engram | One stored memory (MF-001 “trace”) | A debug span |
| Episode | Payload of MemoryType.EPISODIC | A fact |
| Fact | Payload of MemoryType.SEMANTIC | A turn in a slab |
| Skill | Payload of MemoryType.PROCEDURAL | A prompt snippet |
| Encoding header | \(I\), valence, arousal, tags, source, pin, resolved, tombstone, timestamp | SMKM region preamble |
| Strength state | \(D\), \(S\), recall counts, last use | Encoding identity |
| Store | One tier’s persistence | A shared document log |
classDiagram
class Engram {
<<logical>>
MemoryId id
MemoryType type
EncodingHeader header
Payload payload
}
class Episode {
String text
ConversationRole role
long sessionId
int sequence
}
class Fact {
String text
}
class Skill {
String name
List~Step~ steps
}
class Thought {
String text
}
Engram --> EncodingHeader
Engram --> Payload
Payload <|-- Episode
Payload <|-- Fact
Payload <|-- Skill
Payload <|-- Thought
Episode ..> MemoryType : EPISODIC
Fact ..> MemoryType : SEMANTIC
Skill ..> MemoryType : PROCEDURAL
Thought ..> MemoryType : WORKING 2. Problem Statement¶
A robust cognitive architecture requires clean conceptual boundaries between domain operations and underlying physical off-heap storage:
- Monolithic API Surface: Implementing all ingestion, query, consolidation, and administrative methods in a single monolithic implementation class creates tight coupling and makes unit testing individual cognitive pathways unwieldy.
- Ambiguous Store Typing: Treating all memory categories as homogenous byte blobs risks type punning and prevents type-safe queries on episodic conversations versus semantic assertions.
- Pipeline vs Pathway Divergence: Legacy code mixed procedural
Pipelinepatterns with event-drivenPathwayexecution graphs.
3. Decision Drivers¶
- Facade Delegation over Inheritance: Keep the clean
SpectorMemorypublic interface for consumers while decomposing the internal implementation into focused, single-responsibility delegates. - Strict Cognitive Typing: Distinct physical store classes (
EpisodicMemory,SemanticMemory,ProceduralMemory,WorkingMemory) sharing a common engram identity model. - Explicit Sequence Invariants: Deterministic, verifiable ordering of operations during remember, recall, and consolidation cycles.
- Zero Ambiguity in Data Flow: Direct, linear sequence diagrams for all major cognitive pathways.
4. Considered Options¶
Facade Decomposition: Inheritance vs. Delegation¶
- Inheritance (
DefaultSpectorMemory extends DefaultMemoryRemember...): Rejected. Deep inheritance hierarchies create brittle coupling, diamond-dependency issues, and violate composition principles. - Delegation (
DefaultSpectorMemorycomposing focused delegate classes): Selected. Allows each delegate (DefaultMemoryRemember,DefaultMemoryRecall,DefaultMemoryReflection) to be independently instantiated, mocked, and tested.
5. Decision Outcome¶
Facade Architecture & Decomposition¶
Do not add MemoryEngine. The public surface already exists and is ISP-correct:
SpectorMemory
extends MemoryRemember // write
extends MemoryRecall // read
extends MemoryReflection // sleep / dream / decay
extends MemoryAdminView
DefaultSpectorMemory is too large because it implements all four in one class. Java cannot extend two implementation classes. Compose, then delegate.
classDiagram
class SpectorMemory {
<<interface>>
}
class MemoryRemember {
<<interface>>
remember(id, text, MemoryType, ...)
target() RememberPathway
}
class MemoryRecall {
<<interface>>
recall(query, RecallOptions)
}
class MemoryReflection {
<<interface>>
}
class MemoryAdminView {
<<interface>>
}
SpectorMemory --|> MemoryRemember
SpectorMemory --|> MemoryRecall
SpectorMemory --|> MemoryReflection
SpectorMemory --|> MemoryAdminView
class DefaultMemoryRemember {
remember(...)
}
class DefaultMemoryRecall {
recall(...)
}
class DefaultMemoryReflection
class DefaultMemoryAdminView
DefaultMemoryRemember ..|> MemoryRemember
DefaultMemoryRecall ..|> MemoryRecall
DefaultMemoryReflection ..|> MemoryReflection
DefaultMemoryAdminView ..|> MemoryAdminView
class DefaultSpectorMemory {
-MemoryRemember remember
-MemoryRecall recall
-MemoryReflection reflection
-MemoryAdminView admin
}
DefaultSpectorMemory ..|> SpectorMemory
DefaultSpectorMemory --> DefaultMemoryRemember
DefaultSpectorMemory --> DefaultMemoryRecall
DefaultSpectorMemory --> DefaultMemoryReflection
DefaultSpectorMemory --> DefaultMemoryAdminView DefaultSpectorMemory becomes a thin facade: constructor wires the four impls and shared cortex (stores, indexes, soul). Every SpectorMemory method is one-line delegation.
public final class DefaultSpectorMemory implements SpectorMemory, SpectorMemoryAdmin {
private final MemoryRemember remember;
private final MemoryRecall recall;
private final MemoryReflection reflection;
private final MemoryAdminView admin;
@Override
public void remember(String id, String text, MemoryType type,
MemorySource source, String... tags) {
remember.remember(id, text, type, source, tags);
}
@Override
public List<CognitiveResult> recall(String queryText, RecallOptions options) {
return recall.recall(queryText, options);
}
}
That is Facade + Delegation, not a new API. Callers still depend on SpectorMemory. Tests can construct DefaultMemoryRemember or DefaultMemoryRecall against fakes without booting the other three.
DefaultMemoryRemember.target() returns RememberPathway.
DefaultMemoryRecall calls RecallPathway.
RecallPipeline stays deprecated.
Why not DefaultSpectorMemory extends DefaultMemoryRemember¶
You would still have to implement MemoryRecall / MemoryReflection / MemoryAdminView on the same class, and remember-state would leak into the facade. Four delegates keep construction explicit and keep each impl under a size that can be reviewed.
Shared mutable cortex (EpisodicMemory, SemanticMemory, StrengthRecordMemory, indexes) is passed into all four constructors. The facade owns the cortex lifetime; the delegates do not open their own bundles.
Unified Ingestion Signature¶
spectorMemory.remember(id, text, MemoryType.EPISODIC, source, context, tags);
spectorMemory.remember(id, text, MemoryType.SEMANTIC, source, context, tags);
spectorMemory.remember(id, text, MemoryType.PROCEDURAL, source, context, tags);
No rememberEpisode. Role, session, sequence live on IngestionContext (or the episode payload), not on a second verb.
MemoryRemember.rememberEpisodic(...) may remain as a default that fills IngestionContext and calls remember(..., EPISODIC, ...). Same RememberPathway. Not a second store.
Physical Store Schemas & Header Placements¶
flowchart TB
subgraph API["SpectorMemory"]
Rem["MemoryRemember"]
Rec["MemoryRecall"]
Ref["MemoryReflection"]
end
subgraph Paths["Pathways"]
RP["RememberPathway"]
QP["RecallPathway"]
FP["ReflectPathway"]
end
subgraph Stores["Stores — one payload schema each"]
EM["EpisodicMemory"]
SM["SemanticMemory"]
PM["ProceduralMemory"]
WM["WorkingMemory"]
end
subgraph Faces["Shared faces"]
EH["EncodingHeaderLayout"]
ST["StrengthRecordMemory"]
VX["VectorIndex / VectorSpace"]
LX["MemoryBM25Index"]
DIR["IndexRecordMemory"]
GR["association graphs"]
LIN["LineageRecordMemory"]
end
Rem --> RP
Rec --> QP
Ref --> FP
RP --> EM & SM & PM & WM
RP --> EH & ST & VX & LX & DIR & GR
QP --> LX & VX & GR & DIR
QP --> EM & SM & PM
FP --> EM
FP --> SM
FP --> LIN Working memory may skip a durable encoding header (MF-001 NF6 exemption).
Where the encoding header lives¶
| Store | Placement |
|---|---|
EpisodicMemory | On the episode record, after WalkPrefix, before payload bytes |
SemanticMemory | First 64 bytes of the fact slot |
ProceduralMemory | First 64 bytes of the skill slot |
WorkingMemory | Optional |
Vectors for episodes live in the vector index / vector slab, not behind variable episode text. Strength does not live on the episode record.
Episode record [ WalkPrefix 32B | EncodingHeader 64B | episode bytes ]
Fact slot [ EncodingHeader | INT8 vector | … ]
Skill slot [ EncodingHeader | INT8 vector | … ]
Strength row [ D | S | counts | lastUse ] // RegionId.STRENGTH
Store-Specific Header Accessors¶
One layout. Three accessors. Javadoc states what the 64 bytes hold; the type name states which store computes the address.
classDiagram
class EncodingHeader {
float importance
byte valence
byte arousal
Source source
boolean pinned
boolean resolved
boolean tombstone
Instant experiencedAt
}
class EncodingHeaderLayout {
SIZE = 64
read(addr) EncodingHeader
write(addr, EncodingHeader)
}
class EpisodicMemoryHeaderAccessor {
read(recordOffset) EncodingHeader
}
class SemanticMemoryHeaderAccessor {
read(slot) EncodingHeader
}
class ProceduralMemoryHeaderAccessor {
read(slot) EncodingHeader
}
EpisodicMemoryHeaderAccessor --> EncodingHeaderLayout
SemanticMemoryHeaderAccessor --> EncodingHeaderLayout
ProceduralMemoryHeaderAccessor --> EncodingHeaderLayout EpisodicMemoryHeaderAccessor—addr = recordOffset + WalkPrefix.SIZESemanticMemoryHeaderAccessor—addr = slot * factStrideProceduralMemoryHeaderAccessor—addr = slot * skillStride
Javadoc on all three: encoding identity only (\(I\), valence, arousal, tags, source, flags). Semantic payload is a fact; procedural payload is a skill — that sentence belongs on the store, not the accessor.
Today’s HeaderLayout64 / V2 become EncodingHeaderLayout when renamed. EpisodicFieldAccessor splits into EpisodicMemoryHeaderAccessor + EpisodeCodec.
Strength Region Management¶
Yes: the proposed strength store is AuditRecordMemory on RegionId.AUDIT (id = 4). ADR-0028 already put \(S\), \(D\), recall counts, and last use there. “Audit” reads as a log. Rename the enum and the classes; keep id 4.
| Today | Proposed |
|---|---|
RegionId.AUDIT | RegionId.STRENGTH |
AuditRecordMemory | StrengthRecordMemory |
AuditRecordLayout | StrengthRecordLayout |
Javadoc on RegionId.STRENGTH: per-engram retrieval strength \(D\), storage strength \(S\), and use counters. Encoding identity is not stored here.
RegionId.PROVENANCE_LOG (26) stays separate.
Lineage & Provenance Metadata¶
LineageRecordMemory, same *RecordMemory pattern.
If PROVENANCE_LOG already stores parent ids, use it and do not add a region. Otherwise add RegionId.LINEAGE + LineageRecordMemory.
ReflectPathway writes MemoryType.SEMANTIC + MemorySource.DISTILLED and a lineage row. Parent episodes stay in EpisodicMemory.
Class Model: Stores and Projections¶
classDiagram
class EpisodicMemory {
append(id, header, episode) Location
read(Location) EpisodeRecord
cursor() EpisodeCursor
}
class EpisodeRecord {
WalkPrefix prefix
EncodingHeader header
Episode payload
}
class WalkPrefix {
MemoryId id
Instant timestamp
int payloadBytes
}
class EpisodeCodec {
encode(Episode) byte[]
decode(byte[]) Episode
extractText(byte[]) String
}
class SemanticMemory {
insert(id, header, vector, fact) Location
readHeader(Location) EncodingHeader
}
class ProceduralMemory {
insert(id, header, vector, skill) Location
}
class WorkingMemory {
scratch(text)
}
EpisodicMemory --> EpisodeCodec
EpisodicMemory --> EpisodeRecord
EpisodeRecord --> WalkPrefix
EpisodeRecord --> EncodingHeader
SemanticMemory --> EncodingHeader
ProceduralMemory --> EncodingHeader classDiagram
class IndexRecordMemory {
register(MemoryId, Location)
locate(MemoryId) Location
}
class Location {
MemoryType type
int partition
long recordOffset
int vectorSlot
int graphSlot
}
class StrengthRecordMemory {
init(id, D0, S0)
onRecall(id)
current(id) StrengthState
}
class StrengthState {
float D
float S
int recallCount
}
class LineageRecordMemory {
record(child, parents, op)
}
IndexRecordMemory --> Location
StrengthRecordMemory --> StrengthState EpisodicMemory does not embed, does not BM25, does not decay \(D\). Those stay on RememberPathway after the append.
Cognitive Pathways Architecture¶
classDiagram
class RememberPathway {
remember(Write)
}
class RecallPathway {
recall(Cue) List~CognitiveResult~
}
class ReflectPathway {
consolidate()
}
class LexicalRelay
class DenseRelay
class AssociationRelay
class SalienceRelay
class Hydrator
class Fusion
RecallPathway --> LexicalRelay
RecallPathway --> DenseRelay
RecallPathway --> AssociationRelay
RecallPathway --> SalienceRelay
RecallPathway --> Hydrator
RecallPathway --> Fusion
RememberPathway --> EpisodicMemory
RememberPathway --> SemanticMemory
RememberPathway --> StrengthRecordMemory
Hydrator --> EpisodicMemory
Hydrator --> SemanticMemory
Hydrator --> IndexRecordMemory Relays live under pathway.recall / pathway.remember. They are not a public API. Callers use MemoryRecall / MemoryRemember.
DenseRelay must not drop Location.type != SEMANTIC. Hydration branches on type:
switch loc.type():
EPISODIC -> episodic.read(loc)
SEMANTIC -> semantic.read(loc)
PROCEDURAL -> procedural.read(loc)
WORKING -> working.read(loc)
Never semantic.readHeader(episodeLogOffset).
End-to-End Sequence Diagrams¶
Remember Sequence (MemoryType.EPISODIC)¶
sequenceDiagram
actor Caller
participant SM as SpectorMemory
participant WR as DefaultMemoryRemember
participant RP as RememberPathway
participant HF as HeaderFactory / soul
participant EM as EpisodicMemory
participant ST as StrengthRecordMemory
participant VX as VectorIndex
participant LX as MemoryBM25Index
participant DIR as IndexRecordMemory
Caller->>SM: remember(id, text, EPISODIC, source, context, tags)
SM->>WR: remember(...)
WR->>RP: remember(...)
RP->>HF: encoding header at encode
HF-->>RP: I, valence, arousal, source
RP->>EM: append(prefix, header, episode from context)
RP->>ST: init(D0, S0 ≥ 1)
RP->>VX: embed + add
RP->>LX: index(decoded text)
RP->>DIR: register(EPISODIC, loc)
SM-->>Caller: id Recall Sequence¶
sequenceDiagram
actor Caller
participant SM as SpectorMemory
participant RD as DefaultMemoryRecall
participant QP as RecallPathway
participant G as relays union
participant H as Hydrator
participant ST as StrengthRecordMemory
Caller->>SM: recall(query, options)
SM->>RD: recall(...)
RD->>QP: recall(...)
QP->>G: lexical ∪ dense ∪ association ∪ salience
QP->>QP: hard gates
loop surviving ids
QP->>H: load by Location.type
QP->>ST: current(id)
QP->>QP: fuse
end
QP->>ST: onRecall(returned ids)
SM-->>Caller: List of CognitiveResult Consolidation Sequence (ReflectPathway)¶
sequenceDiagram
participant FP as ReflectPathway
participant EM as EpisodicMemory
participant SM as SemanticMemory
participant LIN as LineageRecordMemory
participant DIR as IndexRecordMemory
FP->>EM: cursor of unconsolidated episodes
FP->>SM: remember distilled fact MemoryType.SEMANTIC
FP->>LIN: child → parent episode ids
FP->>EM: mark parent header consolidated
Note over EM: parents stay recallable
FP->>DIR: register(child) Physical Layout of Episode Records¶
EpisodicMemory region (append-only)
record:
+0 WalkPrefix 32B
+32 EncodingHeader 64B
+96 episode payload N
next = 96 + N
Salience walk: read 64 bytes at +32, jump 96+N. No CBOR.
Facts stay in SemanticMemory. Skills stay in ProceduralMemory. Episode vectors stay in the vector index. \(D\)/\(S\) stay in RegionId.STRENGTH.
6. Pros and Cons of the Options¶
Positive¶
- High Cohesion: Each store and delegate has a single, well-defined operational responsibility.
- Clear Execution Flow: Sequences clearly demarcate WAL append, vector projection, graph expansion, and strength updates.
- Type Safety: Specialized engram types prevent misinterpreting semantic facts as episodic dialogue turns.
Negative / Trade-offs¶
- Class Count: Introduces multiple specialized classes and delegates across
com.spectrayan.spector.memory. - Indirection: Public facade methods involve a lightweight forwarding hop to delegate instances.
7. Implementation Plan¶
Package Organization & Visibility¶
com.spectrayan.spector.memory
├── SpectorMemory
├── DefaultSpectorMemory // facade only
├── api
│ MemoryRemember
│ MemoryRecall
│ MemoryReflection
│ MemoryAdminView
├── remember
│ DefaultMemoryRemember
├── recall
│ DefaultMemoryRecall
├── reflection
│ DefaultMemoryReflection
├── pathway
│ remember / recall / reflect / dream / wander
├── cortex
│ EpisodicMemory
│ SemanticMemory
│ ProceduralMemory
│ WorkingMemory
│ StrengthRecordMemory
│ LineageRecordMemory
│ *HeaderAccessor
├── kernel // mmap, bundles, RegionId
└── model // MemoryType, MemorySource, Cue fields
kernel does not know valence. EpisodicMemory does not own BM25. DefaultSpectorMemory does not implement scoring.
Java Idioms & Coding Patterns¶
| Pattern | Where |
|---|---|
| Facade | SpectorMemory / DefaultSpectorMemory |
| ISP | MemoryRemember, MemoryRecall, MemoryReflection, MemoryAdminView |
| Delegation | facade → four default impls |
| Pathway | RememberPathway, RecallPathway, ReflectPathway |
| Repository | EpisodicMemory, SemanticMemory, ProceduralMemory |
| Strategy | header accessors, recall relays |
| Codec | EpisodeCodec |
| Cursor | EpisodeCursor |
| Ledger | StrengthRecordMemory |
| Type | May | Must not |
|---|---|---|
DefaultSpectorMemory | Wire + delegate | Score, append, embed |
DefaultMemoryRemember | Call RememberPathway | Implement recall |
EpisodicMemory | Append/read episodes | Embed, BM25, decay |
EncodingHeaderLayout | Pack 64 bytes | Know CBOR |
StrengthRecordMemory | \(D\), \(S\), counts | Own payload |
RecallPathway | Gather / gate / hydrate / fuse | Append episodes |
kernel.* | mmap / bundles | Soul, importance |
Class & Subsystem Migration Map¶
| Withdrawn draft name | Use |
|---|---|
MemoryEngine | SpectorMemory |
RememberService / RecallService | RememberPathway / RecallPathway |
rememberEpisode(...) | remember(..., MemoryType.EPISODIC, ...) |
TraceLog / TRACE_LOG | EpisodicMemory |
TraceKind | MemoryType |
HEADER_SLAB as source of truth | header bytes on the episode / fact / skill record |
FactHeaderAccessor | SemanticMemoryHeaderAccessor |
SkillHeaderAccessor | ProceduralMemoryHeaderAccessor |
StrengthLedger | StrengthRecordMemory (RegionId.STRENGTH, was AUDIT) |
LineageLog | LineageRecordMemory |
RecallPipeline | RecallPathway (pipeline remains deprecated) |
Kernel types not renamed in this pass: Memory, AbstractMemory, MemoryHeader (region preamble), MemoryShape, PartitionBundle. They are physical.
Open Design Choices & Trade-off Log¶
- Fact vectors stay inline in the semantic slot, or also go through a shared vector slab.
- Episode payload: CBOR vs packed binary — hidden by
EpisodeCodec. WalkPrefixtimestamp vs header timestamp — write both, header wins for recall.- Lineage in
PROVENANCE_LOGvs a newLINEAGEregion. - How small
DefaultSpectorMemorygets: ifSpectorMemoryAdminstays on the same class, keep admin as the fourth delegate rather than growing the facade again.
8. Code Reference & Verification¶
All structural contracts, memory offsets, and class hierarchies are verified against the codebase:
- Unified Facade:
memory/spector-memory/src/main/java/com/spectrayan/spector/memory/core/SpectorMemory.java - Delegate Implementations:
memory/spector-memory/src/main/java/com/spectrayan/spector/memory/core/DefaultMemoryRemember.javamemory/spector-memory/src/main/java/com/spectrayan/spector/memory/core/DefaultMemoryRecall.javamemory/spector-memory/src/main/java/com/spectrayan/spector/memory/core/DefaultMemoryReflection.java
- Engram Storage Implementations:
memory/spector-kernel/src/main/java/com/spectrayan/spector/kernel/record/EpisodicMemory.javamemory/spector-kernel/src/main/java/com/spectrayan/spector/kernel/record/SemanticMemory.java
- Cognitive Pathway Invocations:
memory/spector-memory/src/main/java/com/spectrayan/spector/memory/cortex/pathway/RememberPathway.javamemory/spector-memory/src/main/java/com/spectrayan/spector/memory/cortex/pathway/RecallPathway.javamemory/spector-memory/src/main/java/com/spectrayan/spector/memory/cortex/pathway/ReflectPathway.java