@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,605 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/faculties/spaced.repetition.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * SpacedRepetition — active memory maintenance via scheduled review.
7
+ *
8
+ * Implements the SM-2 algorithm (SuperMemo 2) for optimal review intervals:
9
+ * - Higher confidence → longer intervals between reviews
10
+ * - Successful review increases confidence and interval
11
+ * - Failed review decreases confidence and resets interval
12
+ *
13
+ * Runs independently of the forgetting curve (active vs passive processes ):
14
+ * - ForgettingCurve = passive decay (always happening)
15
+ * - SpacedRepetition = active reinforcement (periodic review)
16
+ *
17
+ * Part of Shard 2 (Memory Layer) — runs every tick, but only triggers
18
+ * review cycles on schedule.
19
+ */
20
+
21
+ import { logger } from '#core/logger'
22
+ import type {
23
+ Duration,
24
+ Tick,
25
+ SimulationContext,
26
+ ReadonlySimulationState,
27
+ StateCommands,
28
+ } from '#core/types'
29
+ import type { SimulationEngine, EngineResult, CognitiveEngine } from '#cognition/types'
30
+ import type { CognitiveEventSchema } from '#cognition/schema.registry'
31
+ import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
32
+ import type { SemanticIntegrator, Belief } from '#faculties/semantic.engine'
33
+ import type { EpisodicConsolidator } from '#faculties/episodic.consolidator'
34
+ import type { ExecutiveEngine } from '#faculties/executive.engine'
35
+ import { GenerativeModel } from '#cognition/generative.model'
36
+ import { SessionLogger } from '#stem/tracts/session.logger'
37
+
38
+ export interface SpacedRepetitionConfig {
39
+ /** Minimum ticks between review cycles */
40
+ reviewIntervalTicks?: number
41
+ /** Maximum beliefs to review per cycle */
42
+ maxReviewsPerCycle?: number
43
+ /** Minimum confidence before a belief qualifies for review */
44
+ minConfidenceForReview?: number
45
+ /** How much successful review increases confidence (0-1) */
46
+ successBoost?: number
47
+ /** How much failed review decreases confidence (0-1) */
48
+ failurePenalty?: number
49
+ /** Base interval for new beliefs (ticks) */
50
+ baseIntervalTicks?: number
51
+ /** Maximum interval cap (ticks) */
52
+ maxIntervalTicks?: number
53
+ /** Whether to surface beliefs to executive for re-evaluation */
54
+ executiveReviewEnabled?: boolean
55
+ /** Whether to actively rehearse salient episodic memories each review cycle
56
+ * (waking episodic spaced repetition). Default true. */
57
+ episodicRehearsalEnabled?: boolean
58
+ bus?: CognitiveBus
59
+ }
60
+
61
+ export interface ReviewRecord {
62
+ beliefId: string
63
+ /** Current interval length in ticks */
64
+ interval: number
65
+ /** Tick of the last review */
66
+ lastReviewedAt: Tick
67
+ /** Number of successful reviews in a row */
68
+ consecutiveSuccesses: number
69
+ /** Easiness factor (SM-2: 1.3 to 2.5, default 2.5) */
70
+ easinessFactor: number
71
+ }
72
+
73
+ export class SpacedRepetition implements SimulationEngine, CognitiveEngine {
74
+ readonly name = 'spaced-repetition'
75
+
76
+ private _reviewIntervalTicks: number
77
+ private _maxReviewsPerCycle: number
78
+ private _minConfidenceForReview: number
79
+ private _successBoost: number
80
+ private _failurePenalty: number
81
+ private _baseIntervalTicks: number
82
+ private _maxIntervalTicks: number
83
+ private _executiveReviewEnabled: boolean
84
+ private _episodicRehearsalEnabled: boolean
85
+
86
+ private _semanticIntegrator: SemanticIntegrator | null = null
87
+ private _episodicConsolidator: EpisodicConsolidator | null = null
88
+ private _executiveEngine: ExecutiveEngine | null = null
89
+ private _sessionLogger: SessionLogger | null = null
90
+
91
+ /** Review records keyed by beliefId */
92
+ private _reviewRecords = new Map<string, ReviewRecord>()
93
+
94
+ /** Last tick when a review cycle ran */
95
+ private _lastReviewCycleTick: Tick = 0
96
+
97
+ private _bus: CognitiveBus | null = null
98
+ private _restored = false
99
+
100
+ private readonly _model = new GenerativeModel()
101
+
102
+ constructor( config: SpacedRepetitionConfig = {} ){
103
+ this._bus = config.bus ?? null
104
+ this._reviewIntervalTicks = config.reviewIntervalTicks ?? 50
105
+ this._maxReviewsPerCycle = config.maxReviewsPerCycle ?? 5
106
+ this._minConfidenceForReview = config.minConfidenceForReview ?? 0.15
107
+ this._successBoost = config.successBoost ?? 0.05
108
+ this._failurePenalty = config.failurePenalty ?? 0.08
109
+ this._baseIntervalTicks = config.baseIntervalTicks ?? 10
110
+ this._maxIntervalTicks = config.maxIntervalTicks ?? 500
111
+ this._executiveReviewEnabled = config.executiveReviewEnabled ?? false
112
+ this._episodicRehearsalEnabled = config.episodicRehearsalEnabled ?? true
113
+ }
114
+
115
+ attachBus( bus: CognitiveBus ): void { this._bus = bus }
116
+ attachSemanticIntegrator( integrator: SemanticIntegrator ): void { this._semanticIntegrator = integrator }
117
+ attachEpisodicConsolidator( consolidator: EpisodicConsolidator ): void { this._episodicConsolidator = consolidator }
118
+ attachExecutiveEngine( executive: ExecutiveEngine ): void { this._executiveEngine = executive }
119
+ attachSessionLogger( logger: SessionLogger ): void { this._sessionLogger = logger }
120
+
121
+ // ── Engine interface ─────────────────────────────────────
122
+
123
+ private _readConfigFromState( state: ReadonlySimulationState ): void {
124
+ const cfg = state.entities.get('engine-config-spaced-repetition')
125
+ if( !cfg ) return
126
+
127
+ const p = cfg.metadata?.params as Record<string, number | boolean> | undefined
128
+ if( !p ) return
129
+
130
+ if( p.reviewIntervalTicks != null ) this._reviewIntervalTicks = p.reviewIntervalTicks as number
131
+ if( p.maxReviewsPerCycle != null ) this._maxReviewsPerCycle = p.maxReviewsPerCycle as number
132
+ if( p.minConfidenceForReview != null ) this._minConfidenceForReview = p.minConfidenceForReview as number
133
+ if( p.successBoost != null ) this._successBoost = p.successBoost as number
134
+ if( p.failurePenalty != null ) this._failurePenalty = p.failurePenalty as number
135
+ if( p.baseIntervalTicks != null ) this._baseIntervalTicks = p.baseIntervalTicks as number
136
+ if( p.maxIntervalTicks != null ) this._maxIntervalTicks = p.maxIntervalTicks as number
137
+ if( p.executiveReviewEnabled != null ) this._executiveReviewEnabled = p.executiveReviewEnabled as boolean
138
+ if( p.episodicRehearsalEnabled != null ) this._episodicRehearsalEnabled = p.episodicRehearsalEnabled as boolean
139
+ }
140
+
141
+ subscribes(): string[] {
142
+ return [
143
+ 'belief.updated',
144
+ 'executive.prediction.formed',
145
+ 'spaced_repetition.review.completed'
146
+ ]
147
+ }
148
+
149
+ publishes(): CognitiveEventSchema[] {
150
+ return [
151
+ { type: 'spaced_repetition.review.due', version: 1, validate: () => null },
152
+ { type: 'spaced_repetition.review.completed', version: 1, validate: () => null },
153
+ { type: 'spaced_repetition.episodes.rehearsed', version: 1, validate: () => null }
154
+ ]
155
+ }
156
+
157
+ onCognitiveEvent( e: CognitiveEvent ): StateCommands | void {
158
+ this._model.observe( e.type, e.salience )
159
+
160
+ switch( e.type ){
161
+ case 'executive.prediction.formed': {
162
+ const p = e.payload as { predictedDomains: string[]; confidence: number }
163
+ if( p.predictedDomains.includes('memory') )
164
+ // Boost the stream this engine actually publishes salience on (the
165
+ // 'spaced_repetition.state.changed' event observes 'records'), so the
166
+ // executive's memory-precision reaches the workspace. (Was
167
+ // 'spaced_repetition.review_rate' — a stream never observed, so the
168
+ // knob was dead. See METACOGNITION_CYCLE_TODO.md 1d.)
169
+ this._model.setPrecision('spaced_repetition.records', 1.0 + p.confidence * 0.5 )
170
+
171
+ break
172
+ }
173
+
174
+ case 'belief.updated': {
175
+ // When a new belief is formed, initialize its review record
176
+ const payload = e.payload as { total?: number; beliefId?: string }
177
+ if( payload.beliefId && !this._reviewRecords.has( payload.beliefId ) )
178
+ this._initializeRecord( payload.beliefId )
179
+
180
+ break
181
+ }
182
+
183
+ case 'spaced_repetition.review.completed': {
184
+ // External review completion (e.g., from executive decision)
185
+ const payload = e.payload as { beliefId: string; success: boolean; tick: number }
186
+ if( payload.beliefId )
187
+ this._processReviewOutcome( payload.beliefId, payload.success, payload.tick )
188
+
189
+ break
190
+ }
191
+ }
192
+ }
193
+
194
+ snapshot(): Record<string, unknown> {
195
+ // FN9: capture salience and model state for deterministic replay
196
+ return {
197
+ model: this._model.snapshot(),
198
+ reviewRecords: Array.from( this._reviewRecords.entries() ),
199
+ lastReviewCycleTick: this._lastReviewCycleTick,
200
+ }
201
+ }
202
+
203
+ restore( snap: Record<string, unknown> ): void {
204
+ if( !snap ) return
205
+ if( snap.model ) this._model.restore(snap.model as import('#cognition/generative.model').GenerativeModelSnapshot)
206
+ if( snap.reviewRecords ){
207
+ this._reviewRecords.clear()
208
+ for( const [ id, record ] of snap.reviewRecords as [string, ReviewRecord][])
209
+ this._reviewRecords.set( id, record )
210
+ }
211
+
212
+ if( snap.lastReviewCycleTick != null ) this._lastReviewCycleTick = snap.lastReviewCycleTick as number
213
+ }
214
+
215
+ /**
216
+ * Rehydrate review records from persisted 'spaced_repetition_record' entities.
217
+ * Called once on the first tick after snapshot restore.
218
+ */
219
+ private _restoreFromState( state: ReadonlySimulationState ): void {
220
+ for( const entity of state.entities.values() ){
221
+ if( entity.type !== 'spaced_repetition_record') continue
222
+ if( this._reviewRecords.has( entity.id ) ) continue
223
+
224
+ const m = entity.metadata ?? {}
225
+ this._reviewRecords.set( entity.id, {
226
+ beliefId: entity.id,
227
+ interval: (m['interval'] as number) ?? this._baseIntervalTicks,
228
+ lastReviewedAt: (m['lastReviewedAt'] as number) ?? 0,
229
+ consecutiveSuccesses: (m['consecutiveSuccesses'] as number) ?? 0,
230
+ easinessFactor: (m['easinessFactor'] as number) ?? 2.5,
231
+ })
232
+ }
233
+
234
+ logger.info(`[spaced-repetition] restored ${this._reviewRecords.size} review records from snapshot`)
235
+ }
236
+
237
+ async react(
238
+ delta: Duration,
239
+ tick: Tick,
240
+ state: ReadonlySimulationState,
241
+ context: SimulationContext
242
+ ): Promise<EngineResult> {
243
+ this._readConfigFromState( state )
244
+
245
+ // On first tick after snapshot restore, rehydrate review records
246
+ if( !this._restored ){
247
+ this._restoreFromState( state )
248
+ this._restored = true
249
+ }
250
+
251
+ const commands: StateCommands = { set: [], metrics: [] }
252
+
253
+ // Check if it's time for a review cycle
254
+ const ticksSinceLastCycle = tick - this._lastReviewCycleTick
255
+ if( ticksSinceLastCycle >= this._reviewIntervalTicks ){
256
+ await this._runReviewCycle( tick, commands )
257
+ // Episodic rehearsal runs on the same cadence, but only while awake — the
258
+ // DreamSimulator owns sleep-time reactivation, so gating here avoids
259
+ // double-boosting the same memories.
260
+ const sleeping = ( state.metrics.get('state.sleeping') ?? 0 ) > 0
261
+ if( this._episodicRehearsalEnabled && !sleeping )
262
+ this._rehearseEpisodes( tick, commands )
263
+ this._lastReviewCycleTick = tick
264
+ }
265
+
266
+ // Persist all review records
267
+ for( const [ beliefId, record ] of this._reviewRecords.entries() )
268
+ commands.set!.push({
269
+ id: beliefId,
270
+ type: 'spaced_repetition_record',
271
+ updatedAt: tick,
272
+ metadata: {
273
+ interval: record.interval,
274
+ lastReviewedAt: record.lastReviewedAt,
275
+ consecutiveSuccesses: record.consecutiveSuccesses,
276
+ easinessFactor: record.easinessFactor
277
+ }
278
+ })
279
+
280
+ if( !commands.metrics ) commands.metrics = []
281
+ commands.metrics.push(
282
+ [ 'memory.spaced_repetition_records', this._reviewRecords.size ],
283
+ [ 'memory.spaced_repetition_last_cycle', this._lastReviewCycleTick ]
284
+ )
285
+
286
+ // Phase C + F: publish cognitive event — gated by prediction error
287
+ const _bus = this._bus
288
+ if( _bus && this._reviewRecords.size > 0 ){
289
+ const predErr = this._model.observe('spaced_repetition.records', this._reviewRecords.size )
290
+ if( !predErr.gated )
291
+ _bus.publish({
292
+ type: 'spaced_repetition.state.changed',
293
+ version: 1,
294
+ sourceEngine: this.name,
295
+ salience: Math.max(0.2, predErr.salience),
296
+ payload: { totalRecords: this._reviewRecords.size }
297
+ })
298
+ }
299
+
300
+ return { commands }
301
+ }
302
+
303
+ // ── Review cycle ─────────────────────────────────────────
304
+
305
+ private async _runReviewCycle( tick: Tick, commands: StateCommands ): Promise<void> {
306
+ if( !this._semanticIntegrator ) return
307
+
308
+ const allBeliefs = this._semanticIntegrator.getBeliefs()
309
+ const dueForReview: Belief[] = []
310
+
311
+ for( const belief of allBeliefs ){
312
+ if( belief.confidence < this._minConfidenceForReview ) continue
313
+
314
+ const record = this._reviewRecords.get( belief.id )
315
+ if( !record ){
316
+ this._initializeRecord( belief.id, tick )
317
+ // New records are due immediately for first review
318
+ dueForReview.push( belief )
319
+ continue
320
+ }
321
+
322
+ const timeSinceLastReview = tick - record.lastReviewedAt
323
+ if( timeSinceLastReview >= record.interval )
324
+ dueForReview.push( belief )
325
+ }
326
+
327
+ // Sort by priority: lowest confidence first (most at risk)
328
+ dueForReview.sort( ( a, b ) => a.confidence - b.confidence )
329
+
330
+ const toReview = dueForReview.slice( 0, this._maxReviewsPerCycle )
331
+ if( toReview.length === 0 ) return
332
+
333
+ logger.info(`[spaced-repetition] reviewing ${toReview.length} beliefs (${dueForReview.length} due total)`)
334
+
335
+ // Publish due events for external handling
336
+ const _bus = this._bus
337
+ if( _bus )
338
+ for( const belief of toReview )
339
+ _bus.publish({
340
+ type: 'spaced_repetition.review.due',
341
+ version: 1,
342
+ sourceEngine: this.name,
343
+ salience: Math.max(0.3, this._model.observe('spaced_repetition.review_due', 1).salience),
344
+ payload: {
345
+ beliefId: belief.id,
346
+ statement: belief.statement,
347
+ currentConfidence: belief.confidence,
348
+ interval: this._reviewRecords.get(belief.id)?.interval ?? this._baseIntervalTicks,
349
+ tick,
350
+ }
351
+ })
352
+
353
+ // Surface to executive for re-evaluation via introspection
354
+ // This would require extending ExecutiveEngine to accept review tasks
355
+ if( this._executiveReviewEnabled && this._executiveEngine )
356
+ await this._surfaceToExecutive( toReview, tick )
357
+
358
+ // Automatic reinforcement: assume successful recall
359
+ else
360
+ for( const belief of toReview )
361
+ this._processReviewOutcome(belief.id, true, tick, commands)
362
+ }
363
+
364
+ // ── Episodic rehearsal (waking episodic spaced repetition) ───
365
+
366
+ /**
367
+ * Active waking rehearsal of salient episodic memories.
368
+ *
369
+ * Beliefs get scheduled SM-2 review above; episodes previously had only
370
+ * passive decay (ForgettingCurve) plus opportunistic sleep reactivation
371
+ * (DreamSimulator), so an emotionally significant memory the Will happened not
372
+ * to recall would simply fade. This selects the most worth-keeping episodes
373
+ * that are *due* — salient, not retrieved within a review interval, still
374
+ * above the pruning floor — and marks them retrieved. markRetrieved both nudges
375
+ * activation and (via retrievalCount) unlocks the ForgettingCurve's
376
+ * retrievalBoost, so rehearsed memories decay slower and persist.
377
+ */
378
+ private _rehearseEpisodes( tick: Tick, commands: StateCommands ): void {
379
+ const consolidator = this._episodicConsolidator
380
+ if( !consolidator ) return
381
+
382
+ const episodes = consolidator.getAllEpisodes()
383
+ if( episodes.length === 0 ) return
384
+
385
+ const due: Array<{ id: string; score: number }> = []
386
+ for( const ep of episodes ){
387
+ // Leave the nearly-forgotten alone — let the ForgettingCurve retire them.
388
+ if( ep.activationStrength < 0.05 ) continue
389
+ // Due = never retrieved, or not retrieved within a review interval.
390
+ const since = ep.lastRetrievedAt === null ? Infinity : tick - ep.lastRetrievedAt
391
+ if( since < this._reviewIntervalTicks ) continue
392
+
393
+ // Prioritise emotionally significant memories that are starting to fade.
394
+ const emotionalIntensity = this._episodeEmotionalIntensity( ep.emotionalTags )
395
+ due.push( { id: ep.id, score: emotionalIntensity * 0.6 + ( 1 - ep.activationStrength ) * 0.4 } )
396
+ }
397
+ if( due.length === 0 ) return
398
+
399
+ // Highest score first; tie-break by id so rehearsal order is replay-stable.
400
+ due.sort( ( a, b ) => ( b.score - a.score ) || ( a.id < b.id ? -1 : 1 ) )
401
+ const rehearsed = due.slice( 0, this._maxReviewsPerCycle )
402
+
403
+ for( const { id } of rehearsed )
404
+ consolidator.markRetrieved( id, tick )
405
+
406
+ commands.metrics ??= []
407
+ commands.metrics.push( [ 'memory.episodic_rehearsed', rehearsed.length ] )
408
+
409
+ this._bus?.publish({
410
+ type: 'spaced_repetition.episodes.rehearsed',
411
+ version: 1,
412
+ sourceEngine: this.name,
413
+ salience: Math.max( 0.3, this._model.observe('spaced_repetition.episodic_rehearsal', rehearsed.length ).salience ),
414
+ payload: { rehearsed: rehearsed.length }
415
+ })
416
+ }
417
+
418
+ private _episodeEmotionalIntensity( emotionalTags: Record<string, number> ): number {
419
+ const v = Object.values( emotionalTags )
420
+ if( v.length === 0 ) return 0
421
+ return v.reduce( ( s, x ) => s + Math.abs( x ), 0 ) / v.length
422
+ }
423
+
424
+ private _initializeRecord( beliefId: string, tick: Tick = 0 ): void {
425
+ this._reviewRecords.set( beliefId, {
426
+ beliefId,
427
+ interval: this._baseIntervalTicks,
428
+ lastReviewedAt: tick,
429
+ consecutiveSuccesses: 0,
430
+ easinessFactor: 2.5
431
+ })
432
+ }
433
+
434
+ private _processReviewOutcome(
435
+ beliefId: string,
436
+ success: boolean,
437
+ tick: Tick,
438
+ commands?: StateCommands
439
+ ): void {
440
+ const record = this._reviewRecords.get( beliefId )
441
+ if( !record ){
442
+ this._initializeRecord( beliefId, tick )
443
+ // First review always considered success
444
+ this._processReviewOutcome( beliefId, true, tick, commands )
445
+ return
446
+ }
447
+
448
+ const belief = this._semanticIntegrator?.getBeliefs().find( b => b.id === beliefId )
449
+ if( !belief ) return
450
+
451
+ const prevConfidence = belief.confidence
452
+
453
+ if( success ){
454
+ // SM-2 algorithm for interval calculation
455
+ let newInterval: number
456
+ if( record.consecutiveSuccesses === 0 ) newInterval = this._baseIntervalTicks
457
+ else if( record.consecutiveSuccesses === 1 ) newInterval = this._baseIntervalTicks * 2
458
+ else newInterval = Math.round( record.interval * record.easinessFactor )
459
+
460
+ record.interval = Math.min( newInterval, this._maxIntervalTicks )
461
+ record.consecutiveSuccesses++
462
+
463
+ // Easiness factor adjustment (SM-2: EF' = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)))
464
+ // Quality 5 (perfect recall) increases EF, lower qualities decrease
465
+ const quality = 5 // Assume perfect recall for automatic reinforcement
466
+ record.easinessFactor = Math.max( 1.3, record.easinessFactor + ( 0.1 - ( 5 - quality) * 0.08 ) )
467
+
468
+ // Increase confidence
469
+ const newConfidence = Math.min( 1, belief.confidence + this._successBoost )
470
+ belief.confidence = newConfidence
471
+ belief.lastUpdatedAt = tick
472
+
473
+ // Record history if SemanticIntegrator exposes it
474
+ this._recordBeliefHistory( belief, tick, prevConfidence, 'spaced_repetition_reinforced' )
475
+ }
476
+ else {
477
+ // Failed review — reset interval and decrease confidence
478
+ record.interval = this._baseIntervalTicks
479
+ record.consecutiveSuccesses = 0
480
+ record.easinessFactor = Math.max( 1.3, record.easinessFactor - 0.2 )
481
+
482
+ const newConfidence = Math.max( 0.1, belief.confidence - this._failurePenalty )
483
+ belief.confidence = newConfidence
484
+ belief.lastUpdatedAt = tick
485
+
486
+ this._recordBeliefHistory( belief, tick, prevConfidence, 'spaced_repetition_failed' )
487
+ }
488
+
489
+ record.lastReviewedAt = tick
490
+
491
+ this._bus?.publish({
492
+ type: 'spaced_repetition.review.completed',
493
+ version: 1,
494
+ sourceEngine: this.name,
495
+ salience: Math.max( 0.4, Math.abs( belief.confidence - prevConfidence ) ),
496
+ payload: {
497
+ beliefId,
498
+ success,
499
+ oldConfidence: prevConfidence,
500
+ newConfidence: belief.confidence,
501
+ newInterval: record.interval,
502
+ tick
503
+ }
504
+ })
505
+ }
506
+
507
+ /**
508
+ * Record belief history entry (mirrors SemanticIntegrator._recordHistory).
509
+ * This requires SemanticIntegrator to expose a public method for external updates.
510
+ */
511
+ private _recordBeliefHistory(
512
+ belief: Belief,
513
+ tick: Tick,
514
+ prevConfidence: number,
515
+ cause: string
516
+ ): void {
517
+ // This would call into SemanticIntegrator if it exposes a method
518
+ // For now, we rely on the integrator's own decay/update mechanisms
519
+ this._sessionLogger?.write({
520
+ type: 'belief.spaced_repetition',
521
+ tick,
522
+ beliefId: belief.id,
523
+ statement: belief.statement.slice( 0, 100 ),
524
+ oldConfidence: prevConfidence,
525
+ newConfidence: belief.confidence,
526
+ cause
527
+ } as any)
528
+ }
529
+
530
+ private async _surfaceToExecutive( beliefs: Belief[], tick: Tick ): Promise<void> {
531
+ if( !this._executiveEngine ) return
532
+
533
+ // Group beliefs by category for efficient processing
534
+ const byCategory = new Map<string, Belief[]>()
535
+ for( const belief of beliefs ){
536
+ const cat = byCategory.get( belief.category ) ?? []
537
+
538
+ cat.push( belief )
539
+ byCategory.set( belief.category, cat )
540
+ }
541
+
542
+ // For each category, create a review prompt for the executive
543
+ // This would require extending ExecutiveEngine to accept external review requests
544
+ // via a new cognitive event type
545
+
546
+ this._bus?.publish({
547
+ type: 'executive.review.requested',
548
+ version: 1,
549
+ sourceEngine: this.name,
550
+ salience: 0.7,
551
+ payload: {
552
+ beliefsToReview: beliefs.map( b => ({
553
+ id: b.id,
554
+ statement: b.statement,
555
+ category: b.category,
556
+ currentConfidence: b.confidence,
557
+ })),
558
+ tick
559
+ }
560
+ })
561
+ }
562
+
563
+ // ── Public API ───────────────────────────────────────────
564
+
565
+ /**
566
+ * Manually trigger a review for a specific belief.
567
+ * Used when the executive decides to re-evaluate a belief.
568
+ */
569
+ requestReview( beliefId: string, tick: Tick ): void {
570
+ const record = this._reviewRecords.get( beliefId )
571
+ if( record ){
572
+ // Reset interval to force immediate review
573
+ record.interval = 0
574
+ record.lastReviewedAt = tick - this._reviewIntervalTicks
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Get the current review status for a belief.
580
+ */
581
+ getReviewStatus( beliefId: string ): ReviewRecord | undefined {
582
+ return this._reviewRecords.get( beliefId )
583
+ }
584
+
585
+ /**
586
+ * Get all beliefs currently due for review (without triggering cycle).
587
+ */
588
+ getDueForReview(tick: Tick ): Array<{ belief: Belief; record: ReviewRecord }> {
589
+ if( !this._semanticIntegrator ) return []
590
+
591
+ const result: Array<{ belief: Belief; record: ReviewRecord }> = []
592
+ const allBeliefs = this._semanticIntegrator.getBeliefs()
593
+
594
+ for( const belief of allBeliefs ){
595
+ const record = this._reviewRecords.get( belief.id )
596
+ if( !record ) continue
597
+
598
+ const timeSinceLastReview = tick - record.lastReviewedAt
599
+ if( timeSinceLastReview >= record.interval )
600
+ result.push({ belief, record })
601
+ }
602
+
603
+ return result.sort( ( a, b ) => a.belief.confidence - b.belief.confidence )
604
+ }
605
+ }