@mindot/will 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +777 -0
- package/dist/index.d.ts +8144 -0
- package/dist/index.js +26231 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
- package/src/cognition/agency/access.grants.ts +59 -0
- package/src/cognition/agency/competence.codec.ts +89 -0
- package/src/cognition/agency/engines/action.selector.ts +462 -0
- package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
- package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
- package/src/cognition/agency/engines/instruction.intake.ts +54 -0
- package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
- package/src/cognition/agency/engines/reafference.engine.ts +241 -0
- package/src/cognition/agency/execution.primitives.ts +118 -0
- package/src/cognition/agency/index.ts +58 -0
- package/src/cognition/agency/proactive.communicator.ts +274 -0
- package/src/cognition/agency/reconcile.learning.ts +82 -0
- package/src/cognition/agency/schemas/external.ts +59 -0
- package/src/cognition/agency/schemas/innate.ts +111 -0
- package/src/cognition/agency/schemas/repertoire.ts +246 -0
- package/src/cognition/agency/selection.scoring.ts +191 -0
- package/src/cognition/agency/types.ts +165 -0
- package/src/cognition/bus.ts +391 -0
- package/src/cognition/completion.inbox.ts +86 -0
- package/src/cognition/config.mirror.entities.ts +475 -0
- package/src/cognition/conversation.memory.ts +81 -0
- package/src/cognition/event.log.ts +116 -0
- package/src/cognition/event.schemas.ts +568 -0
- package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
- package/src/cognition/faculties/affective.blender.ts +562 -0
- package/src/cognition/faculties/attachment.evaluator.ts +432 -0
- package/src/cognition/faculties/attention.allocator.ts +422 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
- package/src/cognition/faculties/bias.detector.ts +363 -0
- package/src/cognition/faculties/circadian.oscillator.ts +294 -0
- package/src/cognition/faculties/confidence.calibrator.ts +324 -0
- package/src/cognition/faculties/dream.simulator.ts +251 -0
- package/src/cognition/faculties/empathy.simulator.ts +253 -0
- package/src/cognition/faculties/energy.regulator.ts +320 -0
- package/src/cognition/faculties/episodic.consolidator.ts +728 -0
- package/src/cognition/faculties/executive.engine/commands.ts +459 -0
- package/src/cognition/faculties/executive.engine/config.ts +46 -0
- package/src/cognition/faculties/executive.engine/context.ts +605 -0
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
- package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
- package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
- package/src/cognition/faculties/executive.engine/facet.ts +628 -0
- package/src/cognition/faculties/executive.engine/gating.ts +222 -0
- package/src/cognition/faculties/executive.engine/index.ts +6 -0
- package/src/cognition/faculties/executive.engine/messages.ts +102 -0
- package/src/cognition/faculties/executive.engine/parser.ts +380 -0
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
- package/src/cognition/faculties/executive.engine/types.ts +323 -0
- package/src/cognition/faculties/exteroception.ts +338 -0
- package/src/cognition/faculties/forgetting.curve.ts +202 -0
- package/src/cognition/faculties/frustration.evaluator.ts +280 -0
- package/src/cognition/faculties/goal.manager.ts +1008 -0
- package/src/cognition/faculties/inhibition.controller.ts +351 -0
- package/src/cognition/faculties/interoception.ts +417 -0
- package/src/cognition/faculties/introspection.engine.ts +246 -0
- package/src/cognition/faculties/known.entity.tracker.ts +439 -0
- package/src/cognition/faculties/loss.evaluator.ts +224 -0
- package/src/cognition/faculties/moral.evaluator.ts +437 -0
- package/src/cognition/faculties/novelty.detector.ts +254 -0
- package/src/cognition/faculties/persona.consolidator.ts +845 -0
- package/src/cognition/faculties/planning.engine/engine.ts +859 -0
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
- package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
- package/src/cognition/faculties/planning.engine/types.ts +143 -0
- package/src/cognition/faculties/reputation.tracker.ts +294 -0
- package/src/cognition/faculties/reward.evaluator.ts +304 -0
- package/src/cognition/faculties/self.model.updater.ts +677 -0
- package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
- package/src/cognition/faculties/semantic.engine/index.ts +6 -0
- package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
- package/src/cognition/faculties/semantic.engine/types.ts +53 -0
- package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
- package/src/cognition/faculties/social.perception.ts +333 -0
- package/src/cognition/faculties/spaced.repetition.ts +605 -0
- package/src/cognition/faculties/stress.regulator.ts +375 -0
- package/src/cognition/faculties/task.switcher.ts +239 -0
- package/src/cognition/faculties/theory.of.mind.ts +333 -0
- package/src/cognition/faculties/threat.evaluator.ts +312 -0
- package/src/cognition/faculties/working.memory.ts +406 -0
- package/src/cognition/generative.model.ts +344 -0
- package/src/cognition/heartbeat.ts +70 -0
- package/src/cognition/index.ts +291 -0
- package/src/cognition/instruction.handler.ts +146 -0
- package/src/cognition/memory/index.ts +35 -0
- package/src/cognition/memory/vector.adapter.ts +316 -0
- package/src/cognition/memory/vector.content.ts +47 -0
- package/src/cognition/memory/vector.embedder.ts +212 -0
- package/src/cognition/memory/vector.index.ts +459 -0
- package/src/cognition/memory/vector.types.ts +65 -0
- package/src/cognition/orchestrator.ts +142 -0
- package/src/cognition/persona.prior.ts +265 -0
- package/src/cognition/schema.registry.ts +145 -0
- package/src/cognition/senses/audition.engine/engine.ts +976 -0
- package/src/cognition/senses/audition.engine/salience.ts +59 -0
- package/src/cognition/senses/base.sense.engine.ts +117 -0
- package/src/cognition/senses/gustation.engine.ts +24 -0
- package/src/cognition/senses/index.ts +172 -0
- package/src/cognition/senses/olfaction.engine.ts +24 -0
- package/src/cognition/senses/somatosensation.engine.ts +23 -0
- package/src/cognition/senses/vision.engine.ts +23 -0
- package/src/cognition/types.ts +63 -0
- package/src/cognition/utilities/token.tracker.ts +492 -0
- package/src/core/abstracts.ts +60 -0
- package/src/core/async.engine.ts +539 -0
- package/src/core/clock.ts +196 -0
- package/src/core/completion.recorder.ts +122 -0
- package/src/core/conflict.detector.ts +140 -0
- package/src/core/distributed.ts +626 -0
- package/src/core/event.bus.ts +292 -0
- package/src/core/inbound.recorder.ts +100 -0
- package/src/core/index.ts +167 -0
- package/src/core/logger.ts +85 -0
- package/src/core/metrics.ts +107 -0
- package/src/core/orchestrator.ts +625 -0
- package/src/core/replay.ts +589 -0
- package/src/core/scenario.ts +120 -0
- package/src/core/serialization.ts +448 -0
- package/src/core/simulation.ts +197 -0
- package/src/core/snapshot.manager.ts +293 -0
- package/src/core/state.manager.ts +283 -0
- package/src/core/types.ts +274 -0
- package/src/core/utils.ts +82 -0
- package/src/core/wall.clock.ts +23 -0
- package/src/extensions/livestream.ext.ts +570 -0
- package/src/extensions/time.ext.ts +339 -0
- package/src/index.ts +91 -0
- package/src/llm/gate.ts +140 -0
- package/src/llm/index.ts +716 -0
- package/src/llm/summarizer.ts +170 -0
- package/src/llm/wire.contracts.ts +67 -0
- package/src/pma/eval.ts +651 -0
- package/src/pma/index.ts +1082 -0
- package/src/profiles/built-in.ts +7 -0
- package/src/profiles/companion.ts +28 -0
- package/src/profiles/company-brain.ts +54 -0
- package/src/profiles/customer-service.ts +30 -0
- package/src/profiles/game-npc.ts +20 -0
- package/src/profiles/index.ts +42 -0
- package/src/profiles/smart-home.ts +34 -0
- package/src/runners/coherence.runner.ts +49 -0
- package/src/runners/outreach.runner.ts +158 -0
- package/src/runners/social.runner.ts +182 -0
- package/src/runners/thin-shim.runner.ts +195 -0
- package/src/sdk/will.ts +334 -0
- package/src/stem/assembly.audit.ts +88 -0
- package/src/stem/distribution.ts +203 -0
- package/src/stem/guards/identity.coherence.ts +152 -0
- package/src/stem/guards/identity.guard.ts +227 -0
- package/src/stem/index.ts +1145 -0
- package/src/stem/mind.ts +1090 -0
- package/src/stem/tracts/ack.reconciler.ts +55 -0
- package/src/stem/tracts/biography.writer.ts +190 -0
- package/src/stem/tracts/effector.controller.ts +143 -0
- package/src/stem/tracts/health.reporter.ts +113 -0
- package/src/stem/tracts/inbound.queue.ts +54 -0
- package/src/stem/tracts/outbox.controller.ts +167 -0
- package/src/stem/tracts/outbox.writer.ts +133 -0
- package/src/stem/tracts/pma.controller.ts +109 -0
- package/src/stem/tracts/replay.controller.ts +177 -0
- package/src/stem/tracts/sensory.controller.ts +168 -0
- package/src/stem/tracts/session.logger.ts +157 -0
- package/src/stem/tracts/transport/index.ts +38 -0
- package/src/stem/tracts/transport/loopback.transport.ts +86 -0
- package/src/stem/tracts/transport/socketio.transport.ts +200 -0
- package/src/stem/tracts/transport/stream.transport.ts +152 -0
- package/src/stem/tracts/transport/types.ts +193 -0
- package/src/stem/tracts/transport.controller.ts +380 -0
- package/src/types.ts +116 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/cognition/faculties/episodic.consolidator.ts
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* EpisodicConsolidator — converts working memory episodes into long-term
|
|
7
|
+
* episodic memory.
|
|
8
|
+
*
|
|
9
|
+
* Consolidation is selective:
|
|
10
|
+
* - Emotionally intense episodes consolidate more strongly
|
|
11
|
+
* - Repeatedly activated items consolidate
|
|
12
|
+
* - Sleep enhances consolidation (DreamSimulator handles this)
|
|
13
|
+
* - Novel/surprising episodes are prioritized
|
|
14
|
+
*
|
|
15
|
+
* Interfaces with the in-house vector/memory store for persistent storage.
|
|
16
|
+
* (Initial implementation uses an in-memory store; durable persistence
|
|
17
|
+
* is a later phase when the memory bridge is built.)
|
|
18
|
+
*
|
|
19
|
+
* Part of Shard 2 (Memory Layer) — runs every tick, synchronous.
|
|
20
|
+
*
|
|
21
|
+
* VECTOR MEMORY INTEGRATION:
|
|
22
|
+
* - Optional VectorMemoryAdapter for semantic similarity search
|
|
23
|
+
* - Indexes episodes automatically on consolidation
|
|
24
|
+
* - Rebuilds index from _store on snapshot restore
|
|
25
|
+
* - No storage limit (infinite _store, only vector index may have limit)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { logger } from '#core/logger'
|
|
29
|
+
import type {
|
|
30
|
+
Duration,
|
|
31
|
+
Tick,
|
|
32
|
+
SimulationContext,
|
|
33
|
+
ReadonlySimulationState,
|
|
34
|
+
StateCommands,
|
|
35
|
+
SimulationEvent,
|
|
36
|
+
} from '#core/types'
|
|
37
|
+
import type { SimulationEngine, EngineResult, CognitiveEngine } from '#cognition/types'
|
|
38
|
+
import type { CognitiveEventSchema } from '#cognition/schema.registry'
|
|
39
|
+
import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
|
|
40
|
+
import type { VectorMemoryAdapter } from '#memory/vector.adapter'
|
|
41
|
+
import type { EmbeddingProvider } from '#memory/vector.embedder'
|
|
42
|
+
import { GenerativeModel } from '#cognition/generative.model'
|
|
43
|
+
|
|
44
|
+
export interface EpisodicConsolidatorConfig {
|
|
45
|
+
/** Threshold above which a WM item is consolidated */
|
|
46
|
+
consolidationThreshold?: number
|
|
47
|
+
/** How much emotional intensity boosts consolidation (multiplier) */
|
|
48
|
+
emotionBoost?: number
|
|
49
|
+
/** Maximum episodes to consolidate per tick */
|
|
50
|
+
maxPerTick?: number
|
|
51
|
+
/** Optional vector memory adapter for semantic search */
|
|
52
|
+
vectorMemory?: VectorMemoryAdapter
|
|
53
|
+
/** Optional embedding provider (required if vectorMemory provided) */
|
|
54
|
+
embedder?: EmbeddingProvider
|
|
55
|
+
/** Whether to automatically index episodes (default true) */
|
|
56
|
+
autoIndex?: boolean
|
|
57
|
+
bus?: CognitiveBus
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface EpisodicMemory {
|
|
61
|
+
id: string
|
|
62
|
+
timestamp: Tick
|
|
63
|
+
content: unknown
|
|
64
|
+
emotionalTags: Record<string, number> // emotion → intensity at time of encoding
|
|
65
|
+
affectiveContext: { valence: number; arousal: number; dominance: number }
|
|
66
|
+
activationStrength: number
|
|
67
|
+
retrievalCount: number
|
|
68
|
+
lastRetrievedAt: Tick | null
|
|
69
|
+
tags: string[]
|
|
70
|
+
sourceType: string // 'percept', 'goal', 'thought', 'interoception'
|
|
71
|
+
/** Wall-clock ms at the moment the episode was first consolidated. */
|
|
72
|
+
createdAt: number
|
|
73
|
+
/**
|
|
74
|
+
* Outcome lifecycle of the originating action/intent:
|
|
75
|
+
* 'intended' — goal or plan was formed but not yet attempted
|
|
76
|
+
* 'attempted' — action was dispatched; outcome unknown at consolidation time
|
|
77
|
+
* 'confirmed' — action was confirmed successful (e.g. message delivered)
|
|
78
|
+
* 'failed' — action failed, timed out, or was abandoned
|
|
79
|
+
*/
|
|
80
|
+
outcomeStatus?: 'intended' | 'attempted' | 'confirmed' | 'failed'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface WMCandidate {
|
|
84
|
+
type: string
|
|
85
|
+
content: unknown
|
|
86
|
+
activation: number
|
|
87
|
+
attendedCount: number
|
|
88
|
+
tags: string[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class EpisodicConsolidator implements SimulationEngine, CognitiveEngine {
|
|
92
|
+
readonly name = 'episodic-consolidator'
|
|
93
|
+
|
|
94
|
+
private _consolidationThreshold: number
|
|
95
|
+
private _emotionBoost: number
|
|
96
|
+
private _maxPerTick: number
|
|
97
|
+
// No maxStoredEpisodes — unlimited storage
|
|
98
|
+
|
|
99
|
+
private _store: EpisodicMemory[] = []
|
|
100
|
+
private _storeMap: Map<string, EpisodicMemory> = new Map()
|
|
101
|
+
private _restored = false
|
|
102
|
+
|
|
103
|
+
/** Ticks between full-store state syncs (captures decay / dream mutations).
|
|
104
|
+
* Must be ≤ SnapshotManager.persistInterval (default 15) so every persisted
|
|
105
|
+
* snapshot contains up-to-date episode values. */
|
|
106
|
+
private readonly _syncInterval = 10
|
|
107
|
+
private _ticksSinceSync = 0
|
|
108
|
+
|
|
109
|
+
private _affectValence: number = 0
|
|
110
|
+
private _affectArousal: number = 0.3
|
|
111
|
+
private _affectDominance: number = 0.5
|
|
112
|
+
|
|
113
|
+
private _bus: CognitiveBus | null = null
|
|
114
|
+
|
|
115
|
+
// Vector memory integration
|
|
116
|
+
private _vectorMemory: VectorMemoryAdapter | null = null
|
|
117
|
+
private _embedder: EmbeddingProvider | null = null
|
|
118
|
+
private _autoIndex: boolean
|
|
119
|
+
|
|
120
|
+
private readonly _model = new GenerativeModel()
|
|
121
|
+
|
|
122
|
+
constructor( config: EpisodicConsolidatorConfig = {} ){
|
|
123
|
+
this._bus = config.bus ?? null
|
|
124
|
+
this._consolidationThreshold = config.consolidationThreshold ?? 0.25
|
|
125
|
+
this._emotionBoost = config.emotionBoost ?? 2.0
|
|
126
|
+
this._maxPerTick = config.maxPerTick ?? 5
|
|
127
|
+
this._vectorMemory = config.vectorMemory ?? null
|
|
128
|
+
this._embedder = config.embedder ?? null
|
|
129
|
+
this._autoIndex = config.autoIndex ?? true
|
|
130
|
+
}
|
|
131
|
+
attachBus( bus: CognitiveBus ): void { this._bus = bus }
|
|
132
|
+
|
|
133
|
+
// ── Engine interface ─────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
private _readConfigFromState( state: ReadonlySimulationState ): void {
|
|
136
|
+
const cfg = state.entities.get('engine-config-episodic')
|
|
137
|
+
if( !cfg ) return
|
|
138
|
+
|
|
139
|
+
const p = cfg.metadata?.params as Record<string, number> | undefined
|
|
140
|
+
if( !p ) return
|
|
141
|
+
|
|
142
|
+
if( p.consolidationThreshold != null ) this._consolidationThreshold = p.consolidationThreshold
|
|
143
|
+
if( p.emotionBoost != null ) this._emotionBoost = p.emotionBoost
|
|
144
|
+
if( p.maxPerTick != null ) this._maxPerTick = p.maxPerTick
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
subscribes(): string[] {
|
|
148
|
+
return [
|
|
149
|
+
'affect.state.changed',
|
|
150
|
+
'executive.prediction.formed'
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
publishes(): CognitiveEventSchema[] { return [] }
|
|
154
|
+
|
|
155
|
+
onCognitiveEvent( e: CognitiveEvent ): StateCommands | void {
|
|
156
|
+
this._model.observe( e.type, e.salience )
|
|
157
|
+
switch( e.type ){
|
|
158
|
+
case 'affect.state.changed': {
|
|
159
|
+
this._affectValence = ( e.payload as Record<string, number> )[ 'valence' ] ?? this._affectValence
|
|
160
|
+
this._affectArousal = ( e.payload as Record<string, number> )[ 'arousal' ] ?? this._affectArousal
|
|
161
|
+
this._affectDominance = ( e.payload as Record<string, number> )[ 'dominance' ] ?? this._affectDominance
|
|
162
|
+
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
case 'executive.prediction.formed': {
|
|
166
|
+
const p = e.payload as { predictedDomains: string[]; confidence: number }
|
|
167
|
+
if( p.predictedDomains.includes('memory') )
|
|
168
|
+
this._model.setPrecision('episode.rate', 1.0 + p.confidence * 0.5 )
|
|
169
|
+
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
snapshot(): Record<string, unknown> {
|
|
176
|
+
return {
|
|
177
|
+
affectValence: this._affectValence,
|
|
178
|
+
affectArousal: this._affectArousal,
|
|
179
|
+
affectDominance: this._affectDominance,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async react(
|
|
184
|
+
_delta: Duration,
|
|
185
|
+
tick: Tick,
|
|
186
|
+
state: ReadonlySimulationState,
|
|
187
|
+
context: SimulationContext
|
|
188
|
+
): Promise<EngineResult> {
|
|
189
|
+
this._readConfigFromState( state )
|
|
190
|
+
|
|
191
|
+
// On first tick after snapshot restore, rehydrate _store from
|
|
192
|
+
// 'episodic_memory' entities persisted in state.
|
|
193
|
+
if( !this._restored ){
|
|
194
|
+
await this._restoreFromState( state )
|
|
195
|
+
this._restored = true
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const
|
|
199
|
+
events: Array<Omit<SimulationEvent, 'id' | 'timestamp' | 'tick'>> = [],
|
|
200
|
+
commands: StateCommands = { metrics: [] }
|
|
201
|
+
|
|
202
|
+
// 1. Scan working memory for consolidation candidates
|
|
203
|
+
const candidates = this._findCandidates( state )
|
|
204
|
+
|
|
205
|
+
// 2. Read current affective state for emotional tagging
|
|
206
|
+
const affectiveContext = {
|
|
207
|
+
valence: state.metrics.get('affect.valence') ?? 0,
|
|
208
|
+
arousal: state.metrics.get('affect.arousal') ?? 0.3,
|
|
209
|
+
dominance: this._affectDominance,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const currentEmotions = this._readCurrentEmotions( state )
|
|
213
|
+
|
|
214
|
+
// 3. Consolidate top candidates
|
|
215
|
+
let consolidated = 0
|
|
216
|
+
const newEpisodes: EpisodicMemory[] = []
|
|
217
|
+
|
|
218
|
+
for( const candidate of candidates ){
|
|
219
|
+
if( consolidated >= this._maxPerTick ) break
|
|
220
|
+
|
|
221
|
+
// Compute consolidation strength
|
|
222
|
+
const
|
|
223
|
+
wmActivation = candidate.activation,
|
|
224
|
+
attendedCount = candidate.attendedCount,
|
|
225
|
+
rehearsalBonus = Math.min( 1, attendedCount / 10 ),
|
|
226
|
+
emotionalIntensity = this._computeEmotionalIntensity( currentEmotions ),
|
|
227
|
+
emotionBonus = emotionalIntensity * this._emotionBoost,
|
|
228
|
+
consolidationStrength = Math.min( 1, wmActivation * 0.4 + rehearsalBonus * 0.3 + emotionBonus * 0.3 )
|
|
229
|
+
|
|
230
|
+
if( consolidationStrength < this._consolidationThreshold ) continue
|
|
231
|
+
|
|
232
|
+
// Infer outcomeStatus from candidate metadata so memories carry their lifecycle.
|
|
233
|
+
const _inferOutcomeStatus = (
|
|
234
|
+
type: string,
|
|
235
|
+
tags: string[],
|
|
236
|
+
): EpisodicMemory['outcomeStatus'] => {
|
|
237
|
+
if( type === 'conversation.exchange' ) return 'confirmed'
|
|
238
|
+
if( tags.includes('failed') || tags.includes('error') || tags.includes('timed_out') ) return 'failed'
|
|
239
|
+
if( tags.includes('completed') || tags.includes('success') || tags.includes('confirmed') ) return 'confirmed'
|
|
240
|
+
if( tags.includes('goal') || tags.includes('plan') ) return 'intended'
|
|
241
|
+
return 'attempted'
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Create episodic memory. createdAt is sim-time ms (state.time), not
|
|
245
|
+
// wall-clock — it persists into entity state and must replay identically (R2).
|
|
246
|
+
const now = state.time
|
|
247
|
+
const episode: EpisodicMemory = {
|
|
248
|
+
id: `episodic-${tick}-${consolidated}`,
|
|
249
|
+
timestamp: tick,
|
|
250
|
+
content: candidate.content,
|
|
251
|
+
emotionalTags: { ...currentEmotions },
|
|
252
|
+
affectiveContext: { ...affectiveContext },
|
|
253
|
+
activationStrength: consolidationStrength,
|
|
254
|
+
retrievalCount: 0,
|
|
255
|
+
lastRetrievedAt: null,
|
|
256
|
+
tags: candidate.tags,
|
|
257
|
+
sourceType: candidate.type,
|
|
258
|
+
createdAt: now,
|
|
259
|
+
outcomeStatus: _inferOutcomeStatus( candidate.type, candidate.tags ),
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this._store.push( episode )
|
|
263
|
+
this._storeMap.set( episode.id, episode )
|
|
264
|
+
newEpisodes.push( episode )
|
|
265
|
+
|
|
266
|
+
commands.set ??= []
|
|
267
|
+
commands.set.push( this._episodeToEntity( episode ) )
|
|
268
|
+
|
|
269
|
+
consolidated++
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 4. Index new episodes in vector memory (no storage limit).
|
|
273
|
+
// Guard only on _vectorMemory — the adapter owns its embedder internally.
|
|
274
|
+
// Works for both HNSW (local embedder) and pgvector (adapter-managed).
|
|
275
|
+
if( this._vectorMemory && this._autoIndex && newEpisodes.length > 0 ){
|
|
276
|
+
const episodesWithContent = newEpisodes.map( ep => ( {
|
|
277
|
+
episode: ep,
|
|
278
|
+
content: ep.content
|
|
279
|
+
} ) )
|
|
280
|
+
|
|
281
|
+
await this._vectorMemory.indexBatch( episodesWithContent )
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 5. Periodic full-store sync — captures activationStrength decay (forgetting curve),
|
|
285
|
+
// emotionalTag dampening (dream simulator), and any other in-memory mutations that
|
|
286
|
+
// other engines apply directly to episodes via getAllEpisodes().
|
|
287
|
+
// Without this, snapshots only ever contain creation-time field values.
|
|
288
|
+
this._ticksSinceSync++
|
|
289
|
+
if( this._ticksSinceSync >= this._syncInterval && this._store.length > 0 ){
|
|
290
|
+
this._ticksSinceSync = 0
|
|
291
|
+
commands.set ??= []
|
|
292
|
+
for( const episode of this._store )
|
|
293
|
+
commands.set.push( this._episodeToEntity( episode ) )
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 6. Metrics (no capacity_used since no limit)
|
|
297
|
+
commands.metrics!.push(
|
|
298
|
+
[ 'memory.episodic_total', this._store.length ],
|
|
299
|
+
[ 'memory.episodic_consolidated', consolidated ],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if( consolidated > 0 && consolidated >= 3 )
|
|
303
|
+
events.push( {
|
|
304
|
+
type: 'memory.consolidation_burst',
|
|
305
|
+
source: this.name,
|
|
306
|
+
payload: { consolidated, storeSize: this._store.length },
|
|
307
|
+
} )
|
|
308
|
+
|
|
309
|
+
// Phase C: publish cognitive event
|
|
310
|
+
const _bus = this._bus
|
|
311
|
+
if( _bus && consolidated > 0 )
|
|
312
|
+
_bus.publish( {
|
|
313
|
+
type: 'episode.consolidated',
|
|
314
|
+
version: 1,
|
|
315
|
+
sourceEngine: this.name,
|
|
316
|
+
salience: Math.max( 0.3, this._model.observe('episode.rate', consolidated ).salience ),
|
|
317
|
+
payload: { total: consolidated }
|
|
318
|
+
} )
|
|
319
|
+
|
|
320
|
+
// Phase D + F: rich state-change event — gated by prediction error
|
|
321
|
+
if( _bus ){
|
|
322
|
+
const predErr = this._model.observe('memory.size', this._store.length )
|
|
323
|
+
if( !predErr.gated )
|
|
324
|
+
_bus.publish( {
|
|
325
|
+
type: 'memory.state.changed',
|
|
326
|
+
version: 1,
|
|
327
|
+
sourceEngine: this.name,
|
|
328
|
+
salience: predErr.salience,
|
|
329
|
+
payload: { episodicTotal: this._store.length }
|
|
330
|
+
} )
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { events: events.length > 0 ? events : undefined, commands }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Public API ───────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Query episodic memory by tags, time range, or emotional context.
|
|
340
|
+
*/
|
|
341
|
+
query( filters: {
|
|
342
|
+
tags?: string[]
|
|
343
|
+
fromTick?: Tick
|
|
344
|
+
toTick?: Tick
|
|
345
|
+
minEmotion?: string
|
|
346
|
+
limit?: number
|
|
347
|
+
} ): EpisodicMemory[] {
|
|
348
|
+
let results = [ ...this._store ]
|
|
349
|
+
|
|
350
|
+
if( filters.tags?.length )
|
|
351
|
+
results = results.filter( m => filters.tags!.some( t => m.tags.includes( t ) ) )
|
|
352
|
+
|
|
353
|
+
if( filters.fromTick !== undefined )
|
|
354
|
+
results = results.filter( m => m.timestamp >= filters.fromTick! )
|
|
355
|
+
|
|
356
|
+
if( filters.toTick !== undefined )
|
|
357
|
+
results = results.filter( m => m.timestamp <= filters.toTick! )
|
|
358
|
+
|
|
359
|
+
if( filters.minEmotion )
|
|
360
|
+
results = results.filter( m => ( m.emotionalTags[ filters.minEmotion! ] ?? 0 ) > 0.3 )
|
|
361
|
+
|
|
362
|
+
// Sort by activation strength (most strongly encoded first), then recency
|
|
363
|
+
results.sort( ( a, b ) => {
|
|
364
|
+
const strengthDiff = b.activationStrength - a.activationStrength
|
|
365
|
+
return strengthDiff !== 0 ? strengthDiff : b.timestamp - a.timestamp
|
|
366
|
+
} )
|
|
367
|
+
|
|
368
|
+
const limit = filters.limit ?? 20
|
|
369
|
+
return results.slice( 0, limit )
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Semantic query via vector memory.
|
|
374
|
+
* Returns episodes with content semantically similar to the query.
|
|
375
|
+
* Requires vectorMemory adapter to be configured.
|
|
376
|
+
*
|
|
377
|
+
* Similarity ranking, with optional mood-congruent re-ranking: when
|
|
378
|
+
* `affectiveBias` is supplied, results are re-scored by blending embedding
|
|
379
|
+
* similarity with affective congruence between the caller's current valence
|
|
380
|
+
* and each episode's encoded valence (`affectiveContext.valence`) — modelling
|
|
381
|
+
* mood-congruent recall (Bower). Similarity still dominates at low weights. To
|
|
382
|
+
* let affect promote a congruent-but-slightly-less-similar memory, we
|
|
383
|
+
* over-fetch candidates and re-rank before truncating to `limit`.
|
|
384
|
+
*
|
|
385
|
+
* Other metadata narrowing (sourceType / tags) remains the caller's job on the
|
|
386
|
+
* returned episodes (which carry all metadata).
|
|
387
|
+
*/
|
|
388
|
+
async semanticQuery(
|
|
389
|
+
query: unknown,
|
|
390
|
+
filters?: {
|
|
391
|
+
minSimilarity?: number
|
|
392
|
+
limit?: number
|
|
393
|
+
/** Mood-congruent re-ranking: target valence [-1,1] + blend weight [0,1]. */
|
|
394
|
+
affectiveBias?: { valence: number; weight: number }
|
|
395
|
+
}
|
|
396
|
+
): Promise<EpisodicMemory[]> {
|
|
397
|
+
if( !this._vectorMemory ){
|
|
398
|
+
logger.warn('[EpisodicConsolidator] semanticQuery called without vectorMemory adapter')
|
|
399
|
+
return []
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const limit = filters?.limit ?? 10
|
|
403
|
+
const bias = filters?.affectiveBias
|
|
404
|
+
const useAffect = !!bias && bias.weight > 0
|
|
405
|
+
|
|
406
|
+
// Over-fetch when re-ranking so affect can promote a congruent memory that
|
|
407
|
+
// ranked just outside the top-`limit` by pure similarity.
|
|
408
|
+
const fetch = useAffect ? Math.min( limit * 3, 50 ) : limit
|
|
409
|
+
|
|
410
|
+
// Timeout guard — recall is BEST-EFFORT. The search embeds the query, which
|
|
411
|
+
// can be a real network call (OpenAI/Google embedder); a hanging endpoint
|
|
412
|
+
// must degrade to "no recall this time", never block a reasoning chain —
|
|
413
|
+
// facet chains await this inside prompt building, so an unbounded hang here
|
|
414
|
+
// stalls the mind's reasoning and everything downstream of it.
|
|
415
|
+
// WILL_RECALL_TIMEOUT_MS (default 5000) — read lazily for tests/live tuning.
|
|
416
|
+
const timeoutMs = parseInt( process.env.WILL_RECALL_TIMEOUT_MS ?? '5000' )
|
|
417
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
418
|
+
const timedOut = Symbol('recall-timeout')
|
|
419
|
+
|
|
420
|
+
const results = await Promise.race( [
|
|
421
|
+
this._vectorMemory.search( query, {
|
|
422
|
+
maxResults: fetch,
|
|
423
|
+
minSimilarity: filters?.minSimilarity,
|
|
424
|
+
} ),
|
|
425
|
+
new Promise<typeof timedOut>( resolve => { timer = setTimeout( () => resolve( timedOut ), timeoutMs ) } ),
|
|
426
|
+
] ).finally( () => clearTimeout( timer ) )
|
|
427
|
+
|
|
428
|
+
if( results === timedOut ){
|
|
429
|
+
logger.warn( `[EpisodicConsolidator] semanticQuery timed out after ${timeoutMs}ms — returning no recall (embedder slow/unreachable?)` )
|
|
430
|
+
return []
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const resolved: Array<{ episode: EpisodicMemory; similarity: number }> = []
|
|
434
|
+
for( const r of results ){
|
|
435
|
+
const episode = this._storeMap.get( r.episodeId )
|
|
436
|
+
if( episode ) resolved.push( { episode, similarity: r.similarity } )
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if( useAffect ){
|
|
440
|
+
const target = Math.max( -1, Math.min( 1, bias!.valence ) )
|
|
441
|
+
const w = Math.max( 0, Math.min( 1, bias!.weight ) )
|
|
442
|
+
// Congruence ∈ [0,1]: 1 when an episode's encoded valence equals the
|
|
443
|
+
// current mood, 0 at opposite poles (|Δ| spans [0,2] for valence∈[-1,1]).
|
|
444
|
+
const score = ( s: { episode: EpisodicMemory; similarity: number } ) =>
|
|
445
|
+
( 1 - w ) * s.similarity
|
|
446
|
+
+ w * ( 1 - Math.abs( s.episode.affectiveContext.valence - target ) / 2 )
|
|
447
|
+
// Tie-break by id keeps the ordering deterministic for replay.
|
|
448
|
+
resolved.sort( ( a, b ) => ( score( b ) - score( a ) ) || ( a.episode.id < b.episode.id ? -1 : 1 ) )
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return resolved.slice( 0, limit ).map( s => s.episode )
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Mark an episode as retrieved (boosts its strength slightly).
|
|
456
|
+
*
|
|
457
|
+
* Immutable replace (mirrors applyDecay): other engines may already hold a
|
|
458
|
+
* reference to this episode this tick, so we update a copy in both _store and
|
|
459
|
+
* _storeMap rather than mutating the shared object underneath them. The bumped
|
|
460
|
+
* retrievalCount is the load-bearing field — it unlocks the ForgettingCurve's
|
|
461
|
+
* retrievalBoost, so memories that are actively recalled decay slower than
|
|
462
|
+
* ones that are never used.
|
|
463
|
+
*/
|
|
464
|
+
markRetrieved( episodeId: string, tick: Tick ): void {
|
|
465
|
+
const episode = this._storeMap.get( episodeId )
|
|
466
|
+
if( !episode ) return
|
|
467
|
+
|
|
468
|
+
const updated: EpisodicMemory = {
|
|
469
|
+
...episode,
|
|
470
|
+
retrievalCount: episode.retrievalCount + 1,
|
|
471
|
+
lastRetrievedAt: tick,
|
|
472
|
+
activationStrength: Math.min( 1, episode.activationStrength + 0.01 ),
|
|
473
|
+
}
|
|
474
|
+
this._storeMap.set( episodeId, updated )
|
|
475
|
+
const idx = this._store.findIndex( e => e.id === episodeId )
|
|
476
|
+
if( idx !== -1 ) this._store[ idx ] = updated
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Get all episodes (for serialization / replay).
|
|
481
|
+
*/
|
|
482
|
+
getAllEpisodes(): ReadonlyArray<EpisodicMemory> {
|
|
483
|
+
return this._store
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Permanently remove decayed episodes from the store, the id index, and the
|
|
488
|
+
* vector index. The consolidator owns the store, so the ForgettingCurve asks
|
|
489
|
+
* it to prune rather than mutating the store itself. Returns the ids that
|
|
490
|
+
* were actually present and removed, so the caller can emit matching state
|
|
491
|
+
* deletions. Removal is order-deterministic for replay.
|
|
492
|
+
*/
|
|
493
|
+
/**
|
|
494
|
+
* Apply decayed activation strengths computed by the ForgettingCurve.
|
|
495
|
+
*
|
|
496
|
+
* The consolidator owns the episode store, so decay is committed here rather
|
|
497
|
+
* than written onto the live objects the curve borrowed via getAllEpisodes()
|
|
498
|
+
* — those references may already be held by other engines this tick. Each
|
|
499
|
+
* changed episode is replaced with an updated copy (immutable update), so
|
|
500
|
+
* previously handed-out references are not mutated underneath their holders.
|
|
501
|
+
*/
|
|
502
|
+
applyDecay( updates: ReadonlyMap<string, number> ): void {
|
|
503
|
+
if( updates.size === 0 ) return
|
|
504
|
+
|
|
505
|
+
this._store = this._store.map( episode => {
|
|
506
|
+
const next = updates.get( episode.id )
|
|
507
|
+
if( next === undefined || next === episode.activationStrength ) return episode
|
|
508
|
+
const updated = { ...episode, activationStrength: next }
|
|
509
|
+
this._storeMap.set( episode.id, updated )
|
|
510
|
+
return updated
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Commit dream-state mutations computed by the DreamSimulator — reactivation
|
|
516
|
+
* boosts (activationStrength), REM emotional dampening (emotionalTags), and
|
|
517
|
+
* creative-recombination tag cross-pollination (tags).
|
|
518
|
+
*
|
|
519
|
+
* Like applyDecay, the consolidator owns the store, so the simulator computes
|
|
520
|
+
* the new field values on its own working copies and hands them here for an
|
|
521
|
+
* immutable replace, rather than mutating the shared episode objects it
|
|
522
|
+
* borrowed via getAllEpisodes() — those references may be held by other
|
|
523
|
+
* engines this tick. Only the fields present in each entry are replaced.
|
|
524
|
+
*/
|
|
525
|
+
applyDreamUpdates(
|
|
526
|
+
updates: ReadonlyMap<string, { activationStrength?: number; emotionalTags?: Record<string, number>; tags?: string[] }>
|
|
527
|
+
): void {
|
|
528
|
+
if( updates.size === 0 ) return
|
|
529
|
+
|
|
530
|
+
this._store = this._store.map( episode => {
|
|
531
|
+
const u = updates.get( episode.id )
|
|
532
|
+
if( !u ) return episode
|
|
533
|
+
const updated: EpisodicMemory = {
|
|
534
|
+
...episode,
|
|
535
|
+
...( u.activationStrength !== undefined ? { activationStrength: u.activationStrength } : {} ),
|
|
536
|
+
...( u.emotionalTags !== undefined ? { emotionalTags: u.emotionalTags } : {} ),
|
|
537
|
+
...( u.tags !== undefined ? { tags: u.tags } : {} ),
|
|
538
|
+
}
|
|
539
|
+
this._storeMap.set( episode.id, updated )
|
|
540
|
+
return updated
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async pruneEpisodes( ids: Iterable<string> ): Promise<string[]> {
|
|
545
|
+
const requested = new Set<string>()
|
|
546
|
+
for( const id of ids ) requested.add( id )
|
|
547
|
+
if( requested.size === 0 ) return []
|
|
548
|
+
|
|
549
|
+
// _store is the authoritative list (what getAllEpisodes exposes), so key
|
|
550
|
+
// removal off it rather than _storeMap — some restore paths populate only
|
|
551
|
+
// the array. removed[] is in store order, keeping replay deterministic.
|
|
552
|
+
const removed: string[] = []
|
|
553
|
+
this._store = this._store.filter( e => {
|
|
554
|
+
if( requested.has( e.id ) ){ removed.push( e.id ); return false }
|
|
555
|
+
return true
|
|
556
|
+
})
|
|
557
|
+
if( removed.length === 0 ) return []
|
|
558
|
+
|
|
559
|
+
for( const id of removed ){
|
|
560
|
+
this._storeMap.delete( id )
|
|
561
|
+
if( this._vectorMemory )
|
|
562
|
+
await this._vectorMemory.delete( id ).catch( () => {} )
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return removed
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Force an immediate full sync of all in-memory episodes to StateCommands.
|
|
570
|
+
*
|
|
571
|
+
* Called at session end (pauseWill / archiveWill) to guarantee that episode
|
|
572
|
+
* mutations accumulated since the last periodic sync — activationStrength
|
|
573
|
+
* decay, emotionalTag dampening, retrieval counts — are captured in the
|
|
574
|
+
* final persisted snapshot. Without this, any session that ends between
|
|
575
|
+
* two scheduled sync ticks loses those mutations on the next cold-start.
|
|
576
|
+
*/
|
|
577
|
+
flushToState(): StateCommands {
|
|
578
|
+
const commands: StateCommands = { set: [] }
|
|
579
|
+
for( const episode of this._store )
|
|
580
|
+
commands.set!.push( this._episodeToEntity( episode ) )
|
|
581
|
+
this._ticksSinceSync = 0
|
|
582
|
+
return commands
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Restore episodes from snapshot (for replay).
|
|
587
|
+
*/
|
|
588
|
+
restoreEpisodes( episodes: EpisodicMemory[] ): void {
|
|
589
|
+
this._store = episodes.map( e => ( {
|
|
590
|
+
...e,
|
|
591
|
+
// Restored episodes carry their original sim-time createdAt; the fallback
|
|
592
|
+
// is a deterministic sentinel (0), never wall-clock, so replay is stable (R2).
|
|
593
|
+
createdAt: e.createdAt ?? 0,
|
|
594
|
+
} ) )
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ── Internal helpers ─────────────────────────────────────
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Serialize one episode into a StateCommands entity write.
|
|
601
|
+
* Used both at creation time and during periodic sync.
|
|
602
|
+
*/
|
|
603
|
+
private _episodeToEntity( episode: EpisodicMemory ): NonNullable<StateCommands[ 'set' ]>[ number ] {
|
|
604
|
+
return {
|
|
605
|
+
id: episode.id,
|
|
606
|
+
type: 'episodic_memory',
|
|
607
|
+
createdAt: episode.createdAt,
|
|
608
|
+
metadata: {
|
|
609
|
+
content: episode.content,
|
|
610
|
+
affectiveContext: episode.affectiveContext,
|
|
611
|
+
activationStrength: episode.activationStrength,
|
|
612
|
+
emotionalTags: episode.emotionalTags,
|
|
613
|
+
retrievalCount: episode.retrievalCount,
|
|
614
|
+
lastRetrievedAt: episode.lastRetrievedAt,
|
|
615
|
+
tags: episode.tags,
|
|
616
|
+
sourceType: episode.sourceType,
|
|
617
|
+
tick: episode.timestamp,
|
|
618
|
+
createdAt: episode.createdAt,
|
|
619
|
+
},
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Rehydrate _store from 'episodic_memory' entities in state.
|
|
625
|
+
* Called once on first tick after snapshot restore.
|
|
626
|
+
* Also rebuilds vector index if configured.
|
|
627
|
+
*/
|
|
628
|
+
private async _restoreFromState( state: ReadonlySimulationState ): Promise<void> {
|
|
629
|
+
if( this._store.length > 0 ) return // already populated (e.g. restoreEpisodes was called)
|
|
630
|
+
|
|
631
|
+
for( const entity of state.entities.values() ){
|
|
632
|
+
if( entity.type !== 'episodic_memory') continue
|
|
633
|
+
const m = entity.metadata ?? {}
|
|
634
|
+
const episode: EpisodicMemory = {
|
|
635
|
+
id: entity.id,
|
|
636
|
+
timestamp: ( m[ 'tick' ] as number ) ?? 0,
|
|
637
|
+
content: ( m[ 'content' ] as unknown ) ?? '',
|
|
638
|
+
emotionalTags: ( m[ 'emotionalTags' ] as Record<string, number> ) ?? {},
|
|
639
|
+
affectiveContext: ( m[ 'affectiveContext' ] as { valence: number; arousal: number; dominance: number } )
|
|
640
|
+
?? { valence: 0, arousal: 0.3, dominance: 0.5 },
|
|
641
|
+
activationStrength: ( m[ 'activationStrength' ] as number ) ?? 0.5,
|
|
642
|
+
retrievalCount: ( m[ 'retrievalCount' ] as number ) ?? 0,
|
|
643
|
+
lastRetrievedAt: ( m[ 'lastRetrievedAt' ] as number | null ) ?? null,
|
|
644
|
+
tags: ( m[ 'tags' ] as string[] ) ?? [],
|
|
645
|
+
sourceType: ( m[ 'sourceType' ] as string ) ?? 'percept',
|
|
646
|
+
createdAt: ( m[ 'createdAt' ] as number ) ?? entity.createdAt,
|
|
647
|
+
}
|
|
648
|
+
this._store.push( episode )
|
|
649
|
+
this._storeMap.set( entity.id, episode )
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Maintain insertion-order by timestamp
|
|
653
|
+
this._store.sort( ( a, b ) => a.timestamp - b.timestamp )
|
|
654
|
+
|
|
655
|
+
// Vector index: try loading persisted index from disk first.
|
|
656
|
+
// Only rebuild (re-embed everything) if the loaded index is empty —
|
|
657
|
+
// avoids re-embedding on every process restart when the index is already valid.
|
|
658
|
+
if( this._vectorMemory ){
|
|
659
|
+
await this._vectorMemory.load()
|
|
660
|
+
if( this._vectorMemory.size === 0 && this._store.length > 0 ){
|
|
661
|
+
await this._vectorMemory.rebuildFromStore( this._store )
|
|
662
|
+
logger.info( `[episodic] vector index rebuilt with ${this._store.length} episodes` )
|
|
663
|
+
} else if( this._vectorMemory.size > 0 ){
|
|
664
|
+
logger.info( `[episodic] vector index loaded from disk (${this._vectorMemory.size} entries)` )
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
logger.info( `[episodic] restored ${this._store.length} episodes from snapshot` )
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ── Internal ─────────────────────────────────────────────
|
|
672
|
+
|
|
673
|
+
private _findCandidates( state: ReadonlySimulationState ): WMCandidate[] {
|
|
674
|
+
const candidates: WMCandidate[] = []
|
|
675
|
+
|
|
676
|
+
// Build a set of recently consolidated content hashes for deduplication
|
|
677
|
+
const recentHashes = new Set<string>()
|
|
678
|
+
for( const memory of this._store.slice( -20 ) ){
|
|
679
|
+
const contentStr = typeof memory.content === 'string'
|
|
680
|
+
? memory.content
|
|
681
|
+
: JSON.stringify( memory.content )
|
|
682
|
+
recentHashes.add( contentStr.slice( 0, 100 ) ) // Hash first 100 chars
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
for( const entity of state.entities.values() ){
|
|
686
|
+
if( entity.type !== 'working_memory.item') continue
|
|
687
|
+
|
|
688
|
+
const content = entity.metadata
|
|
689
|
+
const contentStr = JSON.stringify( content ).slice( 0, 100 )
|
|
690
|
+
|
|
691
|
+
// Skip if this content was recently consolidated (deduplication)
|
|
692
|
+
if( recentHashes.has( contentStr ) ) continue
|
|
693
|
+
|
|
694
|
+
// Skip meta-percepts (percepts about other percepts)
|
|
695
|
+
const category = entity.metadata?.tags as string[] | undefined
|
|
696
|
+
if( category && ( category.includes('episodic_memory') || category.includes('percept') || category.includes('percept.social') ) ) continue
|
|
697
|
+
|
|
698
|
+
candidates.push( {
|
|
699
|
+
type: ( entity.metadata?.wmType as string ) ?? 'unknown',
|
|
700
|
+
content: entity.metadata,
|
|
701
|
+
activation: ( entity.metadata?.activation as number ) ?? 0,
|
|
702
|
+
attendedCount: ( entity.metadata?.attendedCount as number ) ?? 0,
|
|
703
|
+
tags: ( entity.metadata?.tags as string[] ) ?? [],
|
|
704
|
+
} )
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
candidates.sort( ( a, b ) => b.activation - a.activation )
|
|
708
|
+
return candidates
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
private _readCurrentEmotions( state: ReadonlySimulationState ): Record<string, number> {
|
|
712
|
+
const emotions: Record<string, number> = {}
|
|
713
|
+
for( const [ key, value ] of state.metrics ){
|
|
714
|
+
if( key.startsWith('emotion.') )
|
|
715
|
+
emotions[ key.replace('emotion.', '') ] = value
|
|
716
|
+
}
|
|
717
|
+
return emotions
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private _computeEmotionalIntensity( emotions: Record<string, number> ): number {
|
|
721
|
+
const values = Object.values( emotions )
|
|
722
|
+
if( values.length === 0 ) return 0
|
|
723
|
+
|
|
724
|
+
// Emotional intensity = mean of absolute emotion values
|
|
725
|
+
// (Both positive and negative emotions boost consolidation)
|
|
726
|
+
return values.reduce( ( s, v ) => s + Math.abs( v ), 0 ) / values.length
|
|
727
|
+
}
|
|
728
|
+
}
|