@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.
Files changed (178) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +777 -0
  3. package/dist/index.d.ts +8144 -0
  4. package/dist/index.js +26231 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +73 -0
  7. package/src/cognition/agency/access.grants.ts +59 -0
  8. package/src/cognition/agency/competence.codec.ts +89 -0
  9. package/src/cognition/agency/engines/action.selector.ts +462 -0
  10. package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
  11. package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
  12. package/src/cognition/agency/engines/instruction.intake.ts +54 -0
  13. package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
  14. package/src/cognition/agency/engines/reafference.engine.ts +241 -0
  15. package/src/cognition/agency/execution.primitives.ts +118 -0
  16. package/src/cognition/agency/index.ts +58 -0
  17. package/src/cognition/agency/proactive.communicator.ts +274 -0
  18. package/src/cognition/agency/reconcile.learning.ts +82 -0
  19. package/src/cognition/agency/schemas/external.ts +59 -0
  20. package/src/cognition/agency/schemas/innate.ts +111 -0
  21. package/src/cognition/agency/schemas/repertoire.ts +246 -0
  22. package/src/cognition/agency/selection.scoring.ts +191 -0
  23. package/src/cognition/agency/types.ts +165 -0
  24. package/src/cognition/bus.ts +391 -0
  25. package/src/cognition/completion.inbox.ts +86 -0
  26. package/src/cognition/config.mirror.entities.ts +475 -0
  27. package/src/cognition/conversation.memory.ts +81 -0
  28. package/src/cognition/event.log.ts +116 -0
  29. package/src/cognition/event.schemas.ts +568 -0
  30. package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
  31. package/src/cognition/faculties/affective.blender.ts +562 -0
  32. package/src/cognition/faculties/attachment.evaluator.ts +432 -0
  33. package/src/cognition/faculties/attention.allocator.ts +422 -0
  34. package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
  35. package/src/cognition/faculties/bias.detector.ts +363 -0
  36. package/src/cognition/faculties/circadian.oscillator.ts +294 -0
  37. package/src/cognition/faculties/confidence.calibrator.ts +324 -0
  38. package/src/cognition/faculties/dream.simulator.ts +251 -0
  39. package/src/cognition/faculties/empathy.simulator.ts +253 -0
  40. package/src/cognition/faculties/energy.regulator.ts +320 -0
  41. package/src/cognition/faculties/episodic.consolidator.ts +728 -0
  42. package/src/cognition/faculties/executive.engine/commands.ts +459 -0
  43. package/src/cognition/faculties/executive.engine/config.ts +46 -0
  44. package/src/cognition/faculties/executive.engine/context.ts +605 -0
  45. package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
  46. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
  47. package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
  48. package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
  51. package/src/cognition/faculties/executive.engine/facet.ts +628 -0
  52. package/src/cognition/faculties/executive.engine/gating.ts +222 -0
  53. package/src/cognition/faculties/executive.engine/index.ts +6 -0
  54. package/src/cognition/faculties/executive.engine/messages.ts +102 -0
  55. package/src/cognition/faculties/executive.engine/parser.ts +380 -0
  56. package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
  57. package/src/cognition/faculties/executive.engine/types.ts +323 -0
  58. package/src/cognition/faculties/exteroception.ts +338 -0
  59. package/src/cognition/faculties/forgetting.curve.ts +202 -0
  60. package/src/cognition/faculties/frustration.evaluator.ts +280 -0
  61. package/src/cognition/faculties/goal.manager.ts +1008 -0
  62. package/src/cognition/faculties/inhibition.controller.ts +351 -0
  63. package/src/cognition/faculties/interoception.ts +417 -0
  64. package/src/cognition/faculties/introspection.engine.ts +246 -0
  65. package/src/cognition/faculties/known.entity.tracker.ts +439 -0
  66. package/src/cognition/faculties/loss.evaluator.ts +224 -0
  67. package/src/cognition/faculties/moral.evaluator.ts +437 -0
  68. package/src/cognition/faculties/novelty.detector.ts +254 -0
  69. package/src/cognition/faculties/persona.consolidator.ts +845 -0
  70. package/src/cognition/faculties/planning.engine/engine.ts +859 -0
  71. package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
  72. package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
  73. package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
  74. package/src/cognition/faculties/planning.engine/types.ts +143 -0
  75. package/src/cognition/faculties/reputation.tracker.ts +294 -0
  76. package/src/cognition/faculties/reward.evaluator.ts +304 -0
  77. package/src/cognition/faculties/self.model.updater.ts +677 -0
  78. package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
  79. package/src/cognition/faculties/semantic.engine/index.ts +6 -0
  80. package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
  81. package/src/cognition/faculties/semantic.engine/types.ts +53 -0
  82. package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
  83. package/src/cognition/faculties/social.perception.ts +333 -0
  84. package/src/cognition/faculties/spaced.repetition.ts +605 -0
  85. package/src/cognition/faculties/stress.regulator.ts +375 -0
  86. package/src/cognition/faculties/task.switcher.ts +239 -0
  87. package/src/cognition/faculties/theory.of.mind.ts +333 -0
  88. package/src/cognition/faculties/threat.evaluator.ts +312 -0
  89. package/src/cognition/faculties/working.memory.ts +406 -0
  90. package/src/cognition/generative.model.ts +344 -0
  91. package/src/cognition/heartbeat.ts +70 -0
  92. package/src/cognition/index.ts +291 -0
  93. package/src/cognition/instruction.handler.ts +146 -0
  94. package/src/cognition/memory/index.ts +35 -0
  95. package/src/cognition/memory/vector.adapter.ts +316 -0
  96. package/src/cognition/memory/vector.content.ts +47 -0
  97. package/src/cognition/memory/vector.embedder.ts +212 -0
  98. package/src/cognition/memory/vector.index.ts +459 -0
  99. package/src/cognition/memory/vector.types.ts +65 -0
  100. package/src/cognition/orchestrator.ts +142 -0
  101. package/src/cognition/persona.prior.ts +265 -0
  102. package/src/cognition/schema.registry.ts +145 -0
  103. package/src/cognition/senses/audition.engine/engine.ts +976 -0
  104. package/src/cognition/senses/audition.engine/salience.ts +59 -0
  105. package/src/cognition/senses/base.sense.engine.ts +117 -0
  106. package/src/cognition/senses/gustation.engine.ts +24 -0
  107. package/src/cognition/senses/index.ts +172 -0
  108. package/src/cognition/senses/olfaction.engine.ts +24 -0
  109. package/src/cognition/senses/somatosensation.engine.ts +23 -0
  110. package/src/cognition/senses/vision.engine.ts +23 -0
  111. package/src/cognition/types.ts +63 -0
  112. package/src/cognition/utilities/token.tracker.ts +492 -0
  113. package/src/core/abstracts.ts +60 -0
  114. package/src/core/async.engine.ts +539 -0
  115. package/src/core/clock.ts +196 -0
  116. package/src/core/completion.recorder.ts +122 -0
  117. package/src/core/conflict.detector.ts +140 -0
  118. package/src/core/distributed.ts +626 -0
  119. package/src/core/event.bus.ts +292 -0
  120. package/src/core/inbound.recorder.ts +100 -0
  121. package/src/core/index.ts +167 -0
  122. package/src/core/logger.ts +85 -0
  123. package/src/core/metrics.ts +107 -0
  124. package/src/core/orchestrator.ts +625 -0
  125. package/src/core/replay.ts +589 -0
  126. package/src/core/scenario.ts +120 -0
  127. package/src/core/serialization.ts +448 -0
  128. package/src/core/simulation.ts +197 -0
  129. package/src/core/snapshot.manager.ts +293 -0
  130. package/src/core/state.manager.ts +283 -0
  131. package/src/core/types.ts +274 -0
  132. package/src/core/utils.ts +82 -0
  133. package/src/core/wall.clock.ts +23 -0
  134. package/src/extensions/livestream.ext.ts +570 -0
  135. package/src/extensions/time.ext.ts +339 -0
  136. package/src/index.ts +91 -0
  137. package/src/llm/gate.ts +140 -0
  138. package/src/llm/index.ts +716 -0
  139. package/src/llm/summarizer.ts +170 -0
  140. package/src/llm/wire.contracts.ts +67 -0
  141. package/src/pma/eval.ts +651 -0
  142. package/src/pma/index.ts +1082 -0
  143. package/src/profiles/built-in.ts +7 -0
  144. package/src/profiles/companion.ts +28 -0
  145. package/src/profiles/company-brain.ts +54 -0
  146. package/src/profiles/customer-service.ts +30 -0
  147. package/src/profiles/game-npc.ts +20 -0
  148. package/src/profiles/index.ts +42 -0
  149. package/src/profiles/smart-home.ts +34 -0
  150. package/src/runners/coherence.runner.ts +49 -0
  151. package/src/runners/outreach.runner.ts +158 -0
  152. package/src/runners/social.runner.ts +182 -0
  153. package/src/runners/thin-shim.runner.ts +195 -0
  154. package/src/sdk/will.ts +334 -0
  155. package/src/stem/assembly.audit.ts +88 -0
  156. package/src/stem/distribution.ts +203 -0
  157. package/src/stem/guards/identity.coherence.ts +152 -0
  158. package/src/stem/guards/identity.guard.ts +227 -0
  159. package/src/stem/index.ts +1145 -0
  160. package/src/stem/mind.ts +1090 -0
  161. package/src/stem/tracts/ack.reconciler.ts +55 -0
  162. package/src/stem/tracts/biography.writer.ts +190 -0
  163. package/src/stem/tracts/effector.controller.ts +143 -0
  164. package/src/stem/tracts/health.reporter.ts +113 -0
  165. package/src/stem/tracts/inbound.queue.ts +54 -0
  166. package/src/stem/tracts/outbox.controller.ts +167 -0
  167. package/src/stem/tracts/outbox.writer.ts +133 -0
  168. package/src/stem/tracts/pma.controller.ts +109 -0
  169. package/src/stem/tracts/replay.controller.ts +177 -0
  170. package/src/stem/tracts/sensory.controller.ts +168 -0
  171. package/src/stem/tracts/session.logger.ts +157 -0
  172. package/src/stem/tracts/transport/index.ts +38 -0
  173. package/src/stem/tracts/transport/loopback.transport.ts +86 -0
  174. package/src/stem/tracts/transport/socketio.transport.ts +200 -0
  175. package/src/stem/tracts/transport/stream.transport.ts +152 -0
  176. package/src/stem/tracts/transport/types.ts +193 -0
  177. package/src/stem/tracts/transport.controller.ts +380 -0
  178. 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
+ }