@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,1145 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/index.ts — Will lifecycle manager
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // WillStem owns the lifecycle of one or more Will instances:
6
+ // create → tick (autonomous loop) → pause / resume → archive
7
+ //
8
+ // Each Will runs its own async tick loop concurrently.
9
+ // The LLM gate (src/llm/gate.ts) handles concurrency across Wills.
10
+ //
11
+ // Usage (production / API layer):
12
+ // const manager = new WillStem()
13
+ // const id = await manager.createWill(config)
14
+ // manager.addTickListener(id, (snapshot, tick) => { ... })
15
+ // await manager.pauseWill(id)
16
+ // await manager.archiveWill(id)
17
+ // ─────────────────────────────────────────────────────────────
18
+
19
+ import { logger } from '#core/logger'
20
+ import type { DefaultSimulation } from '#core/simulation'
21
+ import type { SimulationEvent, SimulationContext, SimulationState } from '#core/types'
22
+ import type { ExecutiveOutputFull } from '#faculties/executive.engine'
23
+ import type { Cognition, OutboxMessage, effectorInvocation, WorldInterface } from '#types'
24
+ import type { TextMessage, SensoryInput } from '#senses/index'
25
+ import type { ActivityEvent, ActivityEventHandler } from '#cognition/faculties/planning.engine/engine'
26
+ import { assembleMind, type WillConfig } from '#stem/mind'
27
+ import { reviewIdentityCoherence as runCoherenceReview, type CoherenceInput, type CoherenceResult } from '#stem/guards/identity.coherence'
28
+ import { SessionLogger } from '#stem/tracts/session.logger'
29
+ import { fileLoggingEnabled } from '#stem/tracts/transport/stream.transport'
30
+ import type { PMASnapshot } from '#pma/index'
31
+ import type { ReconstructionFidelityReport } from '#pma/eval'
32
+ import type { ReplayMetadata, ReplayComparison } from '#core/replay'
33
+ import { DefaultScenario, type ScenarioConfig, type ScenarioValidationResult } from '#core/scenario'
34
+ import { ReplayController } from '#stem/tracts/replay.controller'
35
+ import { PMAController } from '#stem/tracts/pma.controller'
36
+ import { OutboxController } from '#stem/tracts/outbox.controller'
37
+ import { TransportController } from '#stem/tracts/transport.controller'
38
+ import { InboundQueue } from '#stem/tracts/inbound.queue'
39
+ import type { ExternalTransport } from '#stem/tracts/transport'
40
+ import { effectorController } from '#stem/tracts/effector.controller'
41
+ import { SensoryController } from '#stem/tracts/sensory.controller'
42
+ import { BiographyWriter } from '#stem/tracts/biography.writer'
43
+ import { HealthReporter } from '#stem/tracts/health.reporter'
44
+
45
+ // ── Types ─────────────────────────────────────────────────────
46
+
47
+ export type WillStatus = 'initializing' | 'active' | 'paused' | 'archived'
48
+
49
+ export type StateSnapshot = ReturnType<DefaultSimulation['stateManager']['snapshot']>
50
+
51
+ // outboxMessages + effectorInvocations: snapshots taken before any listener fires.
52
+ // All listeners receive the SAME snapshots so multiple SSE connections each
53
+ // get a complete copy rather than racing on the destructive drain methods.
54
+ export type TickListener = ( snapshot: SimulationState, tick: number, outboxMessages: OutboxMessage[], effectorInvocations: effectorInvocation[] ) => void
55
+
56
+ /**
57
+ * Callback fired for every event published on the simulation's internal
58
+ * DefaultEventBus (via subscribeAll). This gives the API layer access
59
+ * to fine-grained semantic events: goal.formed, belief.updated,
60
+ * emotion.spike, etc. — as published by individual engines.
61
+ */
62
+ export type SimulationEventListener = (
63
+ event: SimulationEvent,
64
+ context: SimulationContext,
65
+ ) => void | Promise<void>
66
+
67
+ export interface CognitiveHealth {
68
+ tick: number
69
+ status: 'healthy' | 'drifting' | 'degraded'
70
+ overallScore: number
71
+ beliefs: {
72
+ total: number
73
+ avgConfidence: number
74
+ highRisk: number
75
+ }
76
+ affect: {
77
+ valence: number
78
+ frustration: number
79
+ irritability: number
80
+ stress: number
81
+ isElevated: boolean
82
+ }
83
+ goals: {
84
+ total: number
85
+ active: number
86
+ }
87
+ }
88
+
89
+ export interface WillSummary {
90
+ id: string
91
+ status: WillStatus
92
+ tickCount: number
93
+ createdAt: Date
94
+ lastTickAt: Date | null
95
+ engineTier: WillConfig['engineTier']
96
+ modelTier: WillConfig['modelTier']
97
+ }
98
+
99
+ // Re-export WillConfig so the API layer only imports from manager
100
+ export type { WillConfig, EngineTier, ModelTier, InitialGoal, WillIdentity } from './mind'
101
+ export type { ExecutiveOutputFull } from '#faculties/executive.engine'
102
+ export type { StorageAdapter } from '#core/abstracts'
103
+ export type { PMASnapshot, PMAIdentity, PMABelief, PMAGoal, PMAEmotionalBaseline, PMABehavioral } from '../pma'
104
+ export { PMADistiller, PMALoader, PMA_SCHEMA_VERSION } from '../pma'
105
+
106
+ // OUTBOX_TTL_TICKS lives in the OutboxController (R5-c); re-exported here so
107
+ // the public barrel (src/index.ts) keeps its existing import surface.
108
+ export { OUTBOX_TTL_TICKS } from '#stem/tracts/outbox.controller'
109
+
110
+ // ── Internal instance record ──────────────────────────────────
111
+
112
+ export interface WillInstance {
113
+ simulation: DefaultSimulation
114
+ cognition: Cognition
115
+ config: WillConfig
116
+ status: WillStatus
117
+ tickCount: number
118
+ createdAt: Date
119
+ lastTickAt: Date | null
120
+ tickListeners: Set<TickListener>
121
+ simulationEventListeners: Set<SimulationEventListener>
122
+ /** Unsubscribe function for the DefaultEventBus subscribeAll hook. */
123
+ _eventBusUnsub: (() => void) | null
124
+ /** Queued outbound messages waiting to be drained by the delivery layer (SSE/webhook). */
125
+ outbox: OutboxMessage[]
126
+ /** External effector invocations pending host-system execution. Drained per tick by SSE. */
127
+ pendingEffectorInvocations: effectorInvocation[]
128
+ /** Bidirectional channel to the host peer (socket.io etc.). null = legacy outbox/SSE path. */
129
+ transport: ExternalTransport | null
130
+ /** Tick-stamped buffer for inbound envelopes; drained + applied at tick start. */
131
+ inbound: InboundQueue
132
+ /** Unsubscribe for the transport's onInbound hook. */
133
+ _transportUnsub: (() => void) | null
134
+ /** NDJSON session log — one file per continuous run (start/resume → pause/archive). */
135
+ sessionLogger: SessionLogger | null
136
+ /** Resolver that interrupts the tick-sleep when a high-priority event (e.g. incoming message) arrives. */
137
+ _tickWakeFn: (() => void) | null
138
+ /** Timestamp when the Will was last paused — used to compute offline duration for the wake percept on resume. */
139
+ pausedAt: Date | null
140
+ /** Set of active LLM-chunk listeners (one per SSE connection). F3 word-level streaming. */
141
+ chunkListeners: Set<( chunk: string ) => void>
142
+ /**
143
+ * Per-entity chunk listeners registered by the SSE/WS layer.
144
+ * Keyed by entityId — only the listener(s) for the active conversation receive chunks.
145
+ * Populated by `addSensoryChunkListener()`.
146
+ */
147
+ sensoryChunkListeners: Map<string, Set<( chunk: string ) => void>>
148
+ /** Running accumulator for the behavioral fingerprint written at session end. */
149
+ _sessionBehavior: {
150
+ startTick: number
151
+ valenceMin: number
152
+ valenceMax: number
153
+ arousalMin: number
154
+ arousalMax: number
155
+ confidenceSum: number
156
+ confidenceCount: number
157
+ goalsTotal: number
158
+ goalsCompleted: number
159
+ // ── Emotional biography tracking ────────────────────────
160
+ /** Previous tick's valence — used for spike delta calculation. */
161
+ prevValence: number
162
+ /** Previous tick's arousal — used for spike delta calculation. */
163
+ prevArousal: number
164
+ /** Valence at session open — for arc reporting. */
165
+ valenceStart: number
166
+ /** Arousal at session open — for arc reporting. */
167
+ arousalStart: number
168
+ /** Valence at last tick — for arc reporting. */
169
+ valenceEnd: number
170
+ /** Arousal at last tick — for arc reporting. */
171
+ arousalEnd: number
172
+ /** Consecutive ticks with arousal > 0.70 (high activation). */
173
+ highArousalStreak: number
174
+ /** Completed sustained high-arousal episodes this session. */
175
+ sustainedEpisodes: number
176
+ /** Total spike events (valence + arousal) this session. */
177
+ spikeCount: number
178
+ /** Sum of all tick valences — for dominant mood computation. */
179
+ avgValenceSum: number
180
+ /** Number of valence samples. */
181
+ avgValenceCount: number
182
+ /** Actions taken in ticks where executive confidence < 0.35. */
183
+ impulsiveActionCount: number
184
+ } | null
185
+ }
186
+
187
+ // ── Manager ───────────────────────────────────────────────────
188
+
189
+ export class WillStem {
190
+ private readonly _wills = new Map<string, WillInstance>()
191
+ // Subsystems extracted to their own collaborators (R5).
192
+ private readonly _replay = new ReplayController() // R5-a: record/replay
193
+ private readonly _pma = new PMAController() // R5-b: distill/load/eval
194
+ private readonly _outbox = new OutboxController() // R5-c: messaging/outbox
195
+ private readonly _effector = new effectorController() // R5-d: external effectors
196
+ private readonly _sensory = new SensoryController() // R5-e: senses I/O + chunk streaming
197
+ private readonly _transport = new TransportController() // External transport ↔ tick boundary
198
+ private readonly _biography = new BiographyWriter() // R5-f: session-biography writers
199
+ private readonly _health = new HealthReporter() // R5-f2: cognitive-health view
200
+
201
+ // ── Create ─────────────────────────────────────────────────
202
+
203
+ /**
204
+ * Assemble a new Will and start its autonomous tick loop.
205
+ * Returns the Will ID (same as config.id) once the loop is running.
206
+ */
207
+ async createWill( config: WillConfig, startPaused = false ): Promise<string> {
208
+ if( this._wills.has( config.id ) )
209
+ throw new Error(`Will already exists: ${config.id}`)
210
+
211
+ const
212
+ { simulation, cognition, outbox } = assembleMind( config.id, config )
213
+
214
+ // ── Snapshot restore ─────────────────────────────────────
215
+ // If persistentMemory is enabled (snapshotStorage configured), attempt to
216
+ // load the most recent persisted snapshot and restore entity state.
217
+ // This brings beliefs, goals, episodic memories, narrative, and bonds
218
+ // back to life across wills restarts without re-running the simulation.
219
+ //
220
+ // NOTE: Metrics are NOT restored — they rebuild naturally from tick 1.
221
+ if( config.persistentMemory || config.snapshotStorage )
222
+ try {
223
+ const previousState = await simulation.snapshotManager.loadLatestFromStorage()
224
+ if( previousState ){
225
+ simulation.stateManager.restore( previousState, { entities: true, metrics: false } )
226
+ logger.info( `[WillStem] Restored snapshot for ${config.id} — ${previousState.entities.size} entities loaded` )
227
+ }
228
+ }
229
+ catch( err ){ logger.warn( `[WillStem] Snapshot restore failed for ${config.id} — starting fresh:`, err ) }
230
+
231
+ const
232
+ dataDir = process.env.WILL_DATA_DIR ?? './data',
233
+ sessionLogger = new SessionLogger( config.id, dataDir, {
234
+ fileLogging: fileLoggingEnabled() && !config.testMode,
235
+ } ),
236
+ instance: WillInstance = {
237
+ simulation,
238
+ cognition,
239
+ config,
240
+ status: 'initializing',
241
+ tickCount: 0,
242
+ createdAt: new Date(),
243
+ lastTickAt: null,
244
+ tickListeners: new Set(),
245
+ simulationEventListeners: new Set(),
246
+ _eventBusUnsub: null,
247
+ outbox,
248
+ pendingEffectorInvocations: [],
249
+ transport: config.transport ?? null,
250
+ inbound: new InboundQueue(),
251
+ _transportUnsub: null,
252
+ sessionLogger,
253
+ _tickWakeFn: null,
254
+ pausedAt: null,
255
+ chunkListeners: new Set(),
256
+ sensoryChunkListeners: new Map(),
257
+ _sessionBehavior: {
258
+ startTick: 0, valenceMin: 1, valenceMax: -1,
259
+ arousalMin: 1, arousalMax: 0,
260
+ confidenceSum: 0, confidenceCount: 0,
261
+ goalsTotal: 0, goalsCompleted: 0,
262
+ prevValence: 0, prevArousal: 0,
263
+ valenceStart: 0, arousalStart: 0,
264
+ valenceEnd: 0, arousalEnd: 0,
265
+ highArousalStreak: 0, sustainedEpisodes: 0,
266
+ spikeCount: 0, avgValenceSum: 0, avgValenceCount: 0,
267
+ impulsiveActionCount: 0
268
+ },
269
+ }
270
+
271
+ // Attach session logger to all cognitive components that produce loggable data
272
+ cognition.executiveEngine.attachSessionLogger( sessionLogger )
273
+ cognition.semanticIntegrator.attachSessionLogger( sessionLogger )
274
+ cognition.motorSchemaExecutor.attachSessionLogger( sessionLogger )
275
+ cognition.outboxWriter.attachSessionLogger( sessionLogger )
276
+ cognition.planningEngine.attachSessionLogger( sessionLogger )
277
+ cognition.goalManager.attachSessionLogger( sessionLogger )
278
+
279
+ // Bridge telemetry onto the configured transport (StreamTransport / SocketIo /
280
+ // Loopback) as 'session_log' / 'token_report' envelopes. No-op when no
281
+ // transport is set; the dev file mirrors still happen inside the producers.
282
+ sessionLogger.attachEmit( record => this._transport.emitSessionLog( instance, record ) )
283
+ cognition.tokenTracker.onRecord( record => this._transport.emitTokenReport( instance, record ) )
284
+
285
+ sessionLogger.write({
286
+ type: 'session.start',
287
+ willId: config.id,
288
+ willName: config.name,
289
+ engineTier: config.engineTier,
290
+ modelTier: config.modelTier,
291
+ startedAt: new Date().toISOString(),
292
+ })
293
+
294
+ // Subscribe to ALL simulation events via DefaultEventBus.subscribeAll().
295
+ // This lets callers (backend SSE, webhook delivery) observe fine-grained
296
+ // semantic events (goal.formed, belief.updated, emotion.spike, etc.)
297
+ // that engines publish during their tick execution.
298
+ instance._eventBusUnsub = simulation.eventBus.subscribeAll( ( event, context ) => {
299
+ // Log every semantic event to the session file
300
+ instance.sessionLogger?.write({
301
+ type: 'event',
302
+ tick: ( event as any ).tick,
303
+ evtType: event.type,
304
+ source: event.source,
305
+ payload: event.payload,
306
+ })
307
+
308
+ // Buffer host-owned effector invocations for delivery. The MotorSchemaExecutor
309
+ // emits `agency.invocation` (intentId = correlation handle) when it holds an
310
+ // external action 'awaiting'; the host executes it and acks → confirmExecution.
311
+ if( event.type === 'agency.invocation' )
312
+ this._effector.bufferInvocation( instance, event.payload as Record<string, unknown> )
313
+
314
+ if( instance.simulationEventListeners.size === 0 ) return
315
+
316
+ for( const fn of instance.simulationEventListeners ){
317
+ try { void fn( event, context ) }
318
+ catch( err ){ logger.error(`[WillStem] sim-event listener error (${config.id}):`, err ) }
319
+ }
320
+ })
321
+
322
+ this._wills.set( config.id, instance )
323
+
324
+ // Wire the external transport's inbound stream onto the tick-stamped queue.
325
+ // No-op when no transport is configured (legacy outbox/SSE path).
326
+ this._transport.attach( instance )
327
+
328
+ // Set initial status before the tick loop starts so the loop's first
329
+ // iteration sees the correct state and skips ticking if paused.
330
+ instance.status = startPaused ? 'paused' : 'active'
331
+
332
+ this._runTickLoop( config.id ).catch( err => {
333
+ logger.error(`[WillStem] tick loop crashed (${config.id}):`, err )
334
+
335
+ const inst = this._wills.get( config.id )
336
+ if( inst ) inst.status = 'archived'
337
+ })
338
+
339
+ return config.id
340
+ }
341
+
342
+ // ── Tick listener ──────────────────────────────────────────
343
+
344
+ /**
345
+ * Register a callback fired after every tick.
346
+ * Use this to attach logging, metrics, or API push in the caller.
347
+ * Returns an unsubscribe function.
348
+ */
349
+ addTickListener( id: string, fn: TickListener ): () => void {
350
+ const instance = this._get( id )
351
+
352
+ instance.tickListeners.add( fn )
353
+ return () => instance.tickListeners.delete( fn )
354
+ }
355
+
356
+ /**
357
+ * Subscribe to semantic events emitted by the simulation's internal
358
+ * DefaultEventBus (via subscribeAll). These are fine-grained events
359
+ * published by individual engines during tick execution:
360
+ *
361
+ * goal.formed — GoalManager created a new goal
362
+ * goal.completed — GoalManager marked a goal completed
363
+ * goal.abandoned — GoalManager abandoned a goal
364
+ * belief.updated — SemanticIntegrator integrated a new belief
365
+ * emotion.spike — AffectiveBlender detected an affect spike
366
+ * executive.complete — ExecutiveEngine finished a reasoning cycle
367
+ * percept.received — Exteroception picked up a new external event
368
+ * dream.consolidated — DreamSimulator ran consolidation
369
+ *
370
+ * Note: engines must explicitly call `simulation.eventBus.publish()`
371
+ * for their events to appear here. Core lifecycle events (tick,
372
+ * flush) always flow through.
373
+ *
374
+ * Returns an unsubscribe function.
375
+ */
376
+ addSimulationEventListener( id: string, fn: SimulationEventListener ): () => void {
377
+ const instance = this._get( id )
378
+ instance.simulationEventListeners.add( fn )
379
+
380
+ return () => instance.simulationEventListeners.delete( fn )
381
+ }
382
+
383
+ // ── Telemetry ────────────────────────────────────────────────
384
+ //
385
+ // Session logs + the token/cost ledger flow out as `session_log` / `token_report`
386
+ // envelopes on the Will's transport. To consume them, pass a StreamTransport as
387
+ // `config.transport` and subscribe on it directly:
388
+ //
389
+ // const transport = new StreamTransport(willId)
390
+ // transport.on('token_report', ({ report }) => meter.charge(report.costUsd))
391
+ // await stem.createWill({ ...config, transport })
392
+
393
+ // ── F3: LLM streaming chunk listeners ────────────────────────
394
+
395
+ /**
396
+ * Subscribe to real-time LLM token chunks for this Will.
397
+ * The callback fires for each text token during executive LLM generation.
398
+ * Returns an unsubscribe function — call it when the SSE connection closes.
399
+ */
400
+ addChunkListener( id: string, fn: ( chunk: string ) => void ): () => void {
401
+ return this._sensory.addChunkListener( this._get( id ), fn )
402
+ }
403
+
404
+ /**
405
+ * Subscribe to real-time LLM token chunks for a specific conversation entity.
406
+ *
407
+ * Unlike `addChunkListener()` (Will-wide), this fires only when the AuditionEngine
408
+ * conversation facet for `entityId` is actively streaming — isolating chunks to the
409
+ * relevant SSE/WS client connection.
410
+ *
411
+ * Implementation: on first listener for any entity, wires `addChunkCallback()` on
412
+ * `AuditionEngine` to fan out chunks by entityId. Subsequent listeners reuse the same
413
+ * callback — no duplicate registration occurs.
414
+ *
415
+ * Returns an unsubscribe function — call it when the SSE/WS connection closes.
416
+ */
417
+ addSensoryChunkListener(
418
+ id: string,
419
+ entityId: string,
420
+ fn: ( chunk: string ) => void,
421
+ ): () => void {
422
+ return this._sensory.addSensoryChunkListener( this._get( id ), entityId, fn )
423
+ }
424
+
425
+ /**
426
+ * Subscribe to plan-activity events for a specific requesting entity.
427
+ *
428
+ * Delegates to `PlanningEngine.addActivityListener()`. Events are emitted
429
+ * for plan lifecycle transitions that were triggered by a message or request
430
+ * from `entityId` (i.e. `requestingEntityId === entityId`).
431
+ *
432
+ * @returns Unsubscribe function.
433
+ */
434
+ addActivityListener(
435
+ id: string,
436
+ entityId: string,
437
+ fn: ActivityEventHandler,
438
+ ): () => void {
439
+ const instance = this._get( id )
440
+ return instance.cognition.planningEngine.addActivityListener( entityId, fn )
441
+ }
442
+
443
+ // ── Lifecycle ──────────────────────────────────────────────
444
+
445
+ pauseWill( id: string ): void {
446
+ const instance = this._get( id )
447
+ if( instance.status !== 'active')
448
+ throw new Error(`Cannot pause Will with status '${instance.status}': ${id}`)
449
+
450
+ // Flush all in-memory episode mutations to state entities, then force a
451
+ // disk persist so the next session cold-starts with every episode intact.
452
+ // Fire-and-forget: pauseWill is sync; the persist completes asynchronously.
453
+ const flushCmds = instance.cognition.episodicConsolidator.flushToState()
454
+ if( flushCmds.set?.length )
455
+ instance.simulation.stateManager.applyCommands( flushCmds )
456
+ const pauseState = instance.simulation.stateManager.snapshot()
457
+ instance.simulation.snapshotManager.persistNow( pauseState )
458
+ .catch( err => logger.error( `[WillStem] snapshot persist failed on pause (${id}):`, err ))
459
+
460
+ this._biography.writeSessionSummary( instance )
461
+ this._biography.writeEmotionalBiographySummary( instance )
462
+ instance.sessionLogger?.close()
463
+ instance.sessionLogger = null
464
+ instance.pausedAt = new Date()
465
+ instance.status = 'paused'
466
+ }
467
+
468
+ resumeWill( id: string ): void {
469
+ const instance = this._get( id )
470
+ if( instance.status !== 'paused')
471
+ throw new Error(`Cannot resume Will with status '${instance.status}': ${id}`)
472
+
473
+ const dataDir = process.env.WILL_DATA_DIR ?? './data'
474
+ const newLogger = new SessionLogger( id, dataDir, {
475
+ fileLogging: fileLoggingEnabled() && !instance.config.testMode,
476
+ } )
477
+ newLogger.attachEmit( record => this._transport.emitSessionLog( instance, record ) )
478
+
479
+ instance.sessionLogger = newLogger
480
+
481
+ instance.cognition.executiveEngine.attachSessionLogger( newLogger )
482
+ instance.cognition.semanticIntegrator.attachSessionLogger( newLogger )
483
+ instance.cognition.motorSchemaExecutor.attachSessionLogger( newLogger )
484
+ instance.cognition.outboxWriter.attachSessionLogger( newLogger )
485
+ instance.cognition.planningEngine.attachSessionLogger( newLogger )
486
+ instance.cognition.goalManager.attachSessionLogger( newLogger )
487
+
488
+ newLogger.write({
489
+ type: 'session.start',
490
+ willId: id,
491
+ willName: instance.config.name,
492
+ resumedAt: new Date().toISOString(),
493
+ tick: instance.tickCount,
494
+ })
495
+
496
+ // ── F2: Wake percept ──────────────────────────────────────
497
+ // Inject a percept so the Will knows it was offline and for how long.
498
+ // This surfaces in the Exteroception → executive context pipeline within
499
+ // the first few ticks after resume, giving the Will situational awareness.
500
+ if( instance.pausedAt ){
501
+ const offlineMs = Date.now() - instance.pausedAt.getTime()
502
+ const offlineMins = Math.round( offlineMs / 60_000 )
503
+ const duration = offlineMins < 2
504
+ ? 'a moment'
505
+ : offlineMins < 60
506
+ ? `${offlineMins} minutes`
507
+ : `${Math.round( offlineMins / 60 )} hours`
508
+
509
+ instance.simulation.stateManager.setEntity({
510
+ id: 'percept-wake-event',
511
+ type: 'percept',
512
+ createdAt: Date.now(),
513
+ updatedAt: Date.now(),
514
+ metadata: {
515
+ category: 'system',
516
+ summary: `I was offline for ${duration}. I am now online again.`,
517
+ salience: 0.75,
518
+ source: 'system',
519
+ offlineMs,
520
+ },
521
+ })
522
+
523
+ instance.pausedAt = null
524
+ }
525
+
526
+ // Reset behavioral accumulator for the new session segment
527
+ instance._sessionBehavior = {
528
+ startTick: instance.tickCount, valenceMin: 1, valenceMax: -1,
529
+ arousalMin: 1, arousalMax: 0,
530
+ confidenceSum: 0, confidenceCount: 0,
531
+ goalsTotal: 0, goalsCompleted: 0,
532
+ prevValence: 0, prevArousal: 0,
533
+ valenceStart: 0, arousalStart: 0,
534
+ valenceEnd: 0, arousalEnd: 0,
535
+ highArousalStreak: 0, sustainedEpisodes: 0,
536
+ spikeCount: 0, avgValenceSum: 0, avgValenceCount: 0,
537
+ impulsiveActionCount: 0,
538
+ }
539
+
540
+ instance.status = 'active'
541
+ }
542
+
543
+ /**
544
+ * Stop a Will permanently. The tick loop exits after the current tick.
545
+ * The instance remains in the map for state inspection.
546
+ */
547
+ async archiveWill( id: string ): Promise<void> {
548
+ const instance = this._get( id )
549
+
550
+ this._biography.writeSessionSummary( instance )
551
+ this._biography.writeEmotionalBiographySummary( instance )
552
+ instance.sessionLogger?.close()
553
+ instance.sessionLogger = null
554
+ instance.status = 'archived'
555
+
556
+ // Clean up the eventBus subscription
557
+ instance._eventBusUnsub?.()
558
+ instance._eventBusUnsub = null
559
+
560
+ // Tear down the external transport + discard un-applied inbound.
561
+ this._transport.detach( instance )
562
+
563
+ // Give the tick loop one tick length to exit cleanly
564
+ const wait = instance.config.tickIntervalMs ?? 1000
565
+ await _sleep( wait + 200 )
566
+
567
+ // Flush all in-memory episode mutations to state entities, then await a
568
+ // final disk persist. The tick loop has exited by now so no concurrent
569
+ // state mutations can race with the flush.
570
+ const flushCmds = instance.cognition.episodicConsolidator.flushToState()
571
+ if( flushCmds.set?.length )
572
+ instance.simulation.stateManager.applyCommands( flushCmds )
573
+ const archiveState = instance.simulation.stateManager.snapshot()
574
+ await instance.simulation.snapshotManager.persistNow( archiveState )
575
+ }
576
+
577
+ // Session-biography writers (behavioral + emotional) extracted to
578
+ // BiographyWriter (R5-f); WillStem calls them from pause/archive and the
579
+ // tick loop's emotion-spike detection.
580
+
581
+ // ── Event injection ────────────────────────────────────────
582
+
583
+ /**
584
+ * Inject a percept or external event into the Will's world.
585
+ * The event is picked up by perceptual engines on the next tick.
586
+ */
587
+ injectEvent( id: string, event: { type: string; payload: Record<string, unknown> } ): void {
588
+ this._sensory.injectEvent( this._get( id ), event )
589
+ }
590
+
591
+ // ── State inspection ───────────────────────────────────────
592
+
593
+ getWillState( id: string ): SimulationState {
594
+ return this._get( id ).simulation.stateManager.snapshot()
595
+ }
596
+
597
+ getWillCognition( id: string ): Cognition {
598
+ return this._get( id ).cognition
599
+ }
600
+
601
+ /**
602
+ * Returns the active session log file path for this Will, or null if
603
+ * the Will is paused/archived (no active session).
604
+ */
605
+ getSessionLogPath( id: string ): string | null {
606
+ const instance = this._wills.get( id )
607
+ return instance?.sessionLogger?.filePath ?? null
608
+ }
609
+
610
+ /**
611
+ * Attach a world interface to a running Will's action executor.
612
+ * Intended for dev use only — call from runner.ts after createWill().
613
+ * In production the Will operates without a server-side world;
614
+ * host-owned effectors are delivered via `agency.invocation` events.
615
+ */
616
+ attachWorld( _id: string, _world: WorldInterface ): void {
617
+ // No-op since the agency cutover: the MotorSchemaExecutor owns enaction. Internal
618
+ // stances run as agency primitives; world/host-owned schemas route out via the
619
+ // `agency.invocation` event (reconciled by reconcileInvocation). Kept for the dev
620
+ // runner's call-site compatibility.
621
+ }
622
+
623
+ /**
624
+ * Returns the most recent executive reasoning output for a Will.
625
+ * Null if the Will hasn't completed its first executive cycle yet.
626
+ */
627
+ getLatestExecutiveOutput( id: string ): ExecutiveOutputFull | null {
628
+ return this._get( id ).cognition.executiveEngine.latestOutput
629
+ }
630
+
631
+ /**
632
+ * Returns a composite health summary for a running Will.
633
+ * Intended for developer dashboards and platform monitoring.
634
+ *
635
+ * Status bands:
636
+ * healthy — normal operating range
637
+ * drifting — one or more indicators approaching problematic thresholds
638
+ * degraded — one or more indicators clearly outside healthy range
639
+ */
640
+ getCognitiveHealth( id: string ): CognitiveHealth {
641
+ return this._health.report( this._get( id ) )
642
+ }
643
+
644
+ /**
645
+ * Load a PMASnapshot into a paused or freshly-created Will.
646
+ *
647
+ * Seeds beliefs, goals, identity, and emotional baseline from the PMA.
648
+ * Should only be called when no prior snapshot has been restored —
649
+ * calling on an active Will with existing beliefs may cause unintended merges.
650
+ *
651
+ * The Will must be paused (status: 'paused' or 'initializing') before calling.
652
+ * Resume the Will after loading to start ticking with the seeded state.
653
+ */
654
+ loadPMA( id: string, pma: PMASnapshot ): void {
655
+ this._pma.load( id, this._get( id ), pma )
656
+ }
657
+
658
+ // ── Replay ─────────────────────────────────────────────────
659
+
660
+ // Record/replay delegates to ReplayController (R5-a). `_get(id)` here both
661
+ // validates the Will exists and supplies the instance the recorder hooks into.
662
+ startReplay( id: string ): string {
663
+ return this._replay.start( id, this._get( id ) )
664
+ }
665
+
666
+ stopReplay( id: string ): Promise<ReplayMetadata> {
667
+ return this._replay.stop( id )
668
+ }
669
+
670
+ getReplayMeta( id: string, runId: string ): ReplayMetadata | null {
671
+ return this._replay.getMeta( id, runId )
672
+ }
673
+
674
+ listReplays( id: string ): ReplayMetadata[] {
675
+ return this._replay.list( id )
676
+ }
677
+
678
+ compareReplays( id: string, runId1: string, runId2: string ): Promise<ReplayComparison> {
679
+ return this._replay.compare( id, runId1, runId2 )
680
+ }
681
+
682
+ // ── Scenario ────────────────────────────────────────────────
683
+
684
+ async loadScenario( id: string, cfg: ScenarioConfig ): Promise<ScenarioValidationResult> {
685
+ const instance = this._get( id )
686
+ const scenario = new DefaultScenario( cfg )
687
+ const result = scenario.validate()
688
+ if( result.isValid )
689
+ await instance.simulation.loadScenario( scenario )
690
+ return result
691
+ }
692
+
693
+ // ── PMA Eval ────────────────────────────────────────────────
694
+
695
+ runPMAEval(
696
+ id: string,
697
+ opts: { behavioral?: boolean; vsOriginal?: boolean } = {}
698
+ ): Promise<ReconstructionFidelityReport> {
699
+ return this._pma.runEval( id, this._get( id ), opts )
700
+ }
701
+
702
+ distillPMA( id: string ): PMASnapshot {
703
+ return this._pma.distill( id, this._get( id ) )
704
+ }
705
+
706
+ /**
707
+ * Optional semantic coherence check for an operator-supplied persona (identity
708
+ * guardrail Phase 2). One LLM review flagging contradictions with the
709
+ * architecture grounding, false-capability claims, and semantic injection the
710
+ * deterministic guard can't catch. Advisory + fail-open (an LLM error returns
711
+ * `ran: false`, never blocks). Intended for the API to call pre-creation.
712
+ */
713
+ reviewIdentityCoherence(
714
+ input: CoherenceInput,
715
+ opts?: { willId?: string },
716
+ ): Promise<CoherenceResult> {
717
+ return runCoherenceReview( input, opts )
718
+ }
719
+
720
+ /**
721
+ * Reset affect metrics to a neutral baseline without touching memory.
722
+ * Intended as a recovery path when a Will is clearly drifting or degraded.
723
+ * Does NOT wipe beliefs, goals, episodes, or the executive's reasoning context.
724
+ */
725
+ recalibrateWill( id: string ): void {
726
+ const
727
+ instance = this._get( id ),
728
+ sm = instance.simulation.stateManager,
729
+
730
+ resetMetrics: Array<[ string, number ]> = [
731
+ [ 'emotion.frustration', 0.0 ],
732
+ [ 'emotion.irritability', 0.0 ],
733
+ [ 'stress.load', 0.0 ],
734
+ [ 'affect.arousal', 0.35 ],
735
+ [ 'affect.valence', 0.10 ],
736
+ [ 'affect.dominant', 0.50 ],
737
+ ]
738
+
739
+ for( const [ key, val ] of resetMetrics )
740
+ sm.setMetric( key, val )
741
+
742
+ instance.sessionLogger?.write({
743
+ type: 'recalibrate',
744
+ tick: instance.tickCount,
745
+ resetMetrics: Object.fromEntries( resetMetrics ),
746
+ })
747
+
748
+ logger.info(`[WillStem] recalibrated affect for Will ${id} at tick ${instance.tickCount}`)
749
+ }
750
+
751
+ /**
752
+ * Returns true if the Will is in the registry (regardless of status).
753
+ */
754
+ isRunning( id: string ): boolean {
755
+ try {
756
+ this._get( id )
757
+ return true
758
+ }
759
+ catch { return false }
760
+ }
761
+
762
+ /**
763
+ * Update the set of allowed communication effectors at runtime.
764
+ * Routes to `AccessGrants.setAllowed()` (the permission / sense gate).
765
+ */
766
+ // ── External effectors (10.1 / 10.3) ─────────────────────────────────────
767
+ // Delegates to effectorController (R5-d). `_get(id)` validates the Will exists
768
+ // and supplies the WillInstance; the effector ops touch only instance fields.
769
+
770
+ /** Update the set of allowed communication effectors at runtime. */
771
+ setAllowedEffectors( id: string, effectors: string[] | null ): void {
772
+ this._effector.setAllowed( this._get( id ), effectors )
773
+ }
774
+
775
+ /**
776
+ * Called by the host/WorldInterface after executing a host-owned effector.
777
+ * `invocationId` is the correlation handle the host echoed (the awaiting
778
+ * `agency.intent` id). Reconciles it into an `agency.outcome` the ReafferenceEngine
779
+ * consumes — learning the result, freeing the intent, and advancing the plan it
780
+ * served (if any). See effectorController.confirmExecution.
781
+ */
782
+ confirmEffectorExecution(
783
+ id: string,
784
+ invocationId: string,
785
+ result: {
786
+ success: boolean
787
+ description: string
788
+ metrics?: Record<string, number>
789
+ },
790
+ ): void {
791
+ this._effector.confirmExecution( this._get( id ), invocationId, result )
792
+ }
793
+
794
+ // ── Messaging / outbox (11.1) ────────────────────────────────────────────
795
+ // Delegates to OutboxController (R5-c). `_get(id)` validates the Will exists
796
+ // and supplies the WillInstance; the outbox ops touch only instance fields.
797
+
798
+ /**
799
+ * Confirm a message was received by the target entity. Writes a
800
+ * message.delivery percept ("ear hears the word you spoke") and updates the
801
+ * conversation.sent entity that tracks the outbox message.
802
+ */
803
+ confirmMessageDelivery( id: string, messageId: string, delivered: boolean ): void {
804
+ this._outbox.confirmDelivery( this._get( id ), messageId, delivered )
805
+ }
806
+
807
+ /**
808
+ * Drain all queued outbound messages from the Will's outbox.
809
+ * Called by the SSE/webhook delivery layer to retrieve messages.
810
+ */
811
+ drainOutbox( id: string ): OutboxMessage[] {
812
+ return this._outbox.drain( this._get( id ) )
813
+ }
814
+
815
+ /** Peek at outbox without draining it. */
816
+ peekOutbox( id: string ): readonly OutboxMessage[] {
817
+ return this._outbox.peek( this._get( id ) )
818
+ }
819
+
820
+ /** Re-queue messages that were drained but not successfully delivered. */
821
+ requeueToOutbox( id: string, messages: OutboxMessage[] ): void {
822
+ this._outbox.requeue( this._get( id ), messages )
823
+ }
824
+
825
+ /**
826
+ * Drain all pending external effector invocations.
827
+ * Called by the SSE delivery layer each tick after drainOutbox.
828
+ */
829
+ drainEffectorInvocations( id: string ): effectorInvocation[] {
830
+ return this._effector.drain( this._get( id ) )
831
+ }
832
+
833
+ // ── Senses API ─────────────────────────────────────────────────
834
+
835
+ /**
836
+ * Route an external text message through Will's AuditionEngine.
837
+ *
838
+ * Senses-aware AuditionEngine handles:
839
+ * - Salience computation (urgency keywords + message length)
840
+ * - Thread digest accumulation (rolling last-5-turn context)
841
+ * - LanguagePercept publication on the CognitiveBus (`senses.audition.percept`)
842
+ * - AttentionAllocator entity salience tracking
843
+ * - Conversation facet spawn / reuse per entityId
844
+ * - LLM reply generation via the conversation facet
845
+ *
846
+ * Reply delivery is handled asynchronously via the outbox / SSE channel.
847
+ * And real-time chunk streaming wiring.
848
+ *
849
+ * @param id Will ID
850
+ * @param input TextMessage — `{ kind: 'text', entityId, threadId, content, speakerName? }`
851
+ */
852
+ async ingestText( id: string, input: TextMessage ): Promise<void> {
853
+ await this._sensory.ingestText( this._get( id ), input )
854
+ }
855
+
856
+ /**
857
+ * Terminate the conversation session for an entity.
858
+ *
859
+ * Destroys the AuditionEngine conversation facet for `entityId`, freeing
860
+ * its slot in the executive facet pool. The thread digest is intentionally
861
+ * preserved — it remains available for history retrieval after session end.
862
+ *
863
+ * Call this when the SSE/WS client disconnects or the session expires.
864
+ *
865
+ * @param id Will ID
866
+ * @param entityId The entity whose conversation session to close
867
+ */
868
+ endConversation( id: string, entityId: string ): void {
869
+ this._get( id ).cognition.auditionEngine.endSession( entityId )
870
+ }
871
+
872
+ /**
873
+ * Return the list of active conversation entityIds for a Will.
874
+ * Each entry is an entityId that currently has a live AuditionEngine facet.
875
+ */
876
+ activeConversationSessions( id: string ): string[] {
877
+ return this._get( id ).cognition.auditionEngine.activeSessions()
878
+ }
879
+
880
+ /**
881
+ * Return registration status for all five sense engines.
882
+ * Shell engines report status: 'shell'; active engines report 'active'.
883
+ */
884
+ getSenseEngineStatus( id: string ): Array<{ domain: string; status: string }> {
885
+ const cog = this._get( id ).cognition
886
+ return [
887
+ cog.auditionEngine,
888
+ cog.visionEngine,
889
+ cog.somatosensationEngine,
890
+ cog.olfactionEngine,
891
+ cog.gustationEngine,
892
+ ].map( engine => {
893
+ const snap = engine.snapshot()
894
+ return {
895
+ domain: ( snap.domain as string ) ?? 'unknown',
896
+ status: ( snap.status as string ) ?? 'active',
897
+ ...(( snap.activeSessions !== undefined ) ? { activeSessions: snap.activeSessions, sessions: snap.sessions } : {}),
898
+ }
899
+ })
900
+ }
901
+
902
+ /**
903
+ * Route a raw SensoryInput to the appropriate sense engine by domain.
904
+ * Used by the debug `POST /senses/:domain/ingest` route.
905
+ * Audition inputs are gated by the 'listen' effector like ingestText().
906
+ */
907
+ async ingestSensory( id: string, domain: string, input: SensoryInput ): Promise<void> {
908
+ await this._sensory.ingestSensory( this._get( id ), domain, input )
909
+ }
910
+
911
+ listWills(): WillSummary[] {
912
+ return Array.from( this._wills.entries() ).map( ([ id, inst ]) => ({
913
+ id,
914
+ status: inst.status,
915
+ tickCount: inst.tickCount,
916
+ createdAt: inst.createdAt,
917
+ lastTickAt: inst.lastTickAt,
918
+ engineTier: inst.config.engineTier,
919
+ modelTier: inst.config.modelTier,
920
+ }))
921
+ }
922
+
923
+ // ── Tick loop (internal) ───────────────────────────────────
924
+
925
+ private async _runTickLoop( id: string ): Promise<void> {
926
+ const
927
+ instance = this._wills.get( id )!,
928
+ tickMs = instance.config.tickIntervalMs ?? 1000,
929
+ maxTicks = instance.config.maxTicks ?? 0
930
+
931
+ while( instance.status !== 'archived'){
932
+ // Pause: spin-wait at low cost until resumed or archived
933
+ if( instance.status === 'paused'){
934
+ await _sleep( 100 )
935
+ continue
936
+ }
937
+
938
+ const start = Date.now()
939
+
940
+ // Apply all inbound external I/O at a fixed point BEFORE the step, stamped
941
+ // to this tick. Result-acks mutate state synchronously here so the step
942
+ // sees them; messages/percepts are fire-and-forget (async reasoning is
943
+ // recorded/replayed by the LLM layer, not the deterministic core).
944
+ this._transport.applyInbound( instance, instance.tickCount, {
945
+ effector: this._effector,
946
+ outbox: this._outbox,
947
+ sensory: this._sensory,
948
+ } )
949
+
950
+ await instance.simulation.step( 1 )
951
+
952
+ instance.tickCount++
953
+ instance.lastTickAt = new Date()
954
+
955
+ // Fire tick listeners with a fresh snapshot.
956
+ // Outbox is snapshotted BEFORE notifying any listener so all active SSE
957
+ // connections receive the same set of outbound messages. The outbox is
958
+ // cleared here; each listener that fails to write may call requeueToOutbox()
959
+ // to preserve messages for the next tick.
960
+ const
961
+ snapshot = instance.simulation.stateManager.snapshot(),
962
+ outboxSnapshot = instance.outbox.splice( 0 ),
963
+ invocationsSnapshot = instance.pendingEffectorInvocations.splice( 0 )
964
+
965
+ for( const msg of outboxSnapshot )
966
+ if( msg.createdAtTick === 0 ) msg.createdAtTick = instance.tickCount
967
+
968
+ // Bridge drained outbox messages + effector invocations onto the transport
969
+ // (2.3 / 2.4). No-op when no transport (legacy SSE tick-listener path).
970
+ // Facet replies are NOT in the outbox when a transport is present — they go
971
+ // via the 2.1 reply fast-path — so emitOutbox carries only master/action messages.
972
+ this._transport.emitOutbox( instance, outboxSnapshot )
973
+ this._transport.emitInvocations( instance, invocationsSnapshot )
974
+
975
+ if( instance.tickListeners.size > 0 )
976
+ for( const fn of instance.tickListeners ){
977
+ try { fn( snapshot, instance.tickCount, outboxSnapshot, invocationsSnapshot ) }
978
+ catch( err ){ logger.error(`[WillStem] tick listener error (${id}):`, err ) }
979
+ }
980
+
981
+ // Session log — per-tick metrics snapshot.
982
+ // MUST_LOG_METRICS: canonical list of signals required for PMA + behavioral analysis.
983
+ // If any of these are absent from the tick log, the engine responsible is not pushing
984
+ // the metric or is using a different key — investigate the engine's metrics push block.
985
+ //
986
+ // Affective: affect.valence, affect.arousal, affect.dominance, intero.mood
987
+ // Homeostasis: energy.level, sleep.pressure, stress.load
988
+ // Memory: memory.episodic_total, memory.beliefs_total
989
+ // Goals: goals.active, goals.avg_progress, goals.top_priority
990
+ // Executive: executive.confidence, executive.epistemic_uncertainty, executive.action_count, executive.action_diversity
991
+ // Self-model: self_model.version
992
+ // Executor: executor.success_rate, executor.actions_this_tick
993
+ if( instance.sessionLogger ){
994
+ const metrics: Record<string, number> = {}
995
+ for( const [ k, v ] of snapshot.metrics ) metrics[ k ] = v
996
+
997
+ const entityCounts: Record<string, number> = {}
998
+ for( const e of snapshot.entities.values() )
999
+ entityCounts[ e.type ] = ( entityCounts[ e.type ] ?? 0 ) + 1
1000
+
1001
+ instance.sessionLogger.write({
1002
+ type: 'tick',
1003
+ tick: instance.tickCount,
1004
+ durationMs: instance.lastTickAt ? Date.now() - instance.lastTickAt.getTime() : 0,
1005
+ outboxSize: instance.outbox.length,
1006
+ entityCounts,
1007
+ metrics
1008
+ })
1009
+ }
1010
+
1011
+ // Behavioral fingerprint + emotional biography accumulator.
1012
+ // Updated every tick so session.end has a complete picture of emotional range,
1013
+ // executive confidence, goal outcomes, and significant emotional events.
1014
+ if( instance._sessionBehavior ){
1015
+ const sb = instance._sessionBehavior
1016
+ const v = snapshot.metrics.get( 'affect.valence' ) ?? 0
1017
+ const a = snapshot.metrics.get( 'affect.arousal' ) ?? 0
1018
+ const c = snapshot.metrics.get( 'executive.confidence' )
1019
+
1020
+ // ── Range tracking ──────────────────────────────────────
1021
+ if( v < sb.valenceMin ) sb.valenceMin = v
1022
+ if( v > sb.valenceMax ) sb.valenceMax = v
1023
+ if( a < sb.arousalMin ) sb.arousalMin = a
1024
+ if( a > sb.arousalMax ) sb.arousalMax = a
1025
+ if( c !== undefined ){ sb.confidenceSum += c; sb.confidenceCount++ }
1026
+ const gt = snapshot.metrics.get( 'goals.total' )
1027
+ const gc = snapshot.metrics.get( 'goals.completed_total' )
1028
+ if( gt !== undefined ) sb.goalsTotal = gt
1029
+ if( gc !== undefined ) sb.goalsCompleted = gc
1030
+
1031
+ // ── Emotional biography: seed arc start on first sample ─
1032
+ if( sb.avgValenceCount === 0 ){
1033
+ sb.valenceStart = v
1034
+ sb.arousalStart = a
1035
+ sb.prevValence = v
1036
+ sb.prevArousal = a
1037
+ }
1038
+
1039
+ sb.avgValenceSum += v
1040
+ sb.avgValenceCount ++
1041
+ sb.valenceEnd = v
1042
+ sb.arousalEnd = a
1043
+
1044
+ // ── Spike detection ─────────────────────────────────────
1045
+ // Write a real-time entry to emotional_biography.jsonl for each spike
1046
+ // so PMA can pin emotional events to specific ticks without scanning
1047
+ // the full session log. Thresholds: |Δv| ≥ 0.12, |Δa| ≥ 0.18.
1048
+ const dv = v - sb.prevValence
1049
+ const da = a - sb.prevArousal
1050
+
1051
+ if( Math.abs( dv ) >= 0.12 ){
1052
+ sb.spikeCount++
1053
+ this._biography.writeEmotionalEvent( instance, 'spike', {
1054
+ dimension: 'valence', from: sb.prevValence, to: v, delta: dv,
1055
+ tick: instance.tickCount,
1056
+ })
1057
+ }
1058
+
1059
+ if( Math.abs( da ) >= 0.18 ){
1060
+ sb.spikeCount++
1061
+ this._biography.writeEmotionalEvent( instance, 'spike', {
1062
+ dimension: 'arousal', from: sb.prevArousal, to: a, delta: da,
1063
+ tick: instance.tickCount,
1064
+ })
1065
+ }
1066
+
1067
+ // ── Sustained high-arousal episodes ─────────────────────
1068
+ // An episode completes when the streak exits (arousal drops back below 0.7)
1069
+ // after ≥ 10 consecutive ticks. Session-end flush handles open streaks.
1070
+ if( a > 0.70 ){
1071
+ sb.highArousalStreak++
1072
+ } else {
1073
+ if( sb.highArousalStreak >= 10 ){
1074
+ sb.sustainedEpisodes++
1075
+ this._biography.writeEmotionalEvent( instance, 'sustained_high_arousal', {
1076
+ startTick: instance.tickCount - sb.highArousalStreak,
1077
+ durationTicks: sb.highArousalStreak,
1078
+ tick: instance.tickCount,
1079
+ })
1080
+ }
1081
+ sb.highArousalStreak = 0
1082
+ }
1083
+
1084
+ sb.prevValence = v
1085
+ sb.prevArousal = a
1086
+
1087
+ // Track impulsive actions: ticks where executive fired with low confidence
1088
+ const conf = snapshot.metrics.get( 'executive.confidence' )
1089
+ const actCount = Math.round( snapshot.metrics.get( 'executive.action_count' ) ?? 0 )
1090
+ if( conf !== undefined && conf < 0.35 && actCount > 0 )
1091
+ sb.impulsiveActionCount += actCount
1092
+ }
1093
+
1094
+ // Expire stale outbox messages (TTL cleanup; OutboxController owns the policy).
1095
+ this._outbox.expireStale( instance )
1096
+
1097
+ // Pause when the per-session tick budget is exhausted.
1098
+ // maxTicks is a run-length limit, not a TTL — the Will stays alive
1099
+ // and can be resumed on the next session for a fresh budget.
1100
+ if( maxTicks > 0 && instance.tickCount >= maxTicks ){
1101
+ instance.status = 'paused'
1102
+ // loop continues spin-waiting in the paused branch above
1103
+ }
1104
+
1105
+ // Maintain target tick rate, but allow early wake when a message arrives.
1106
+ // _tickWakeFn is set by injectIncomingMessage; calling it interrupts the sleep.
1107
+ const
1108
+ elapsed = Date.now() - start,
1109
+ delay = tickMs - elapsed
1110
+
1111
+ if( delay > 0 ){
1112
+ await Promise.race([
1113
+ _sleep( delay ),
1114
+ new Promise<void>( resolve => { instance._tickWakeFn = resolve }),
1115
+ ])
1116
+
1117
+ instance._tickWakeFn = null
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ // ── Helpers ────────────────────────────────────────────────
1123
+
1124
+ private _get( id: string ): WillInstance {
1125
+ const instance = this._wills.get( id )
1126
+ if( !instance ) throw new Error(`Will not found: ${id}`)
1127
+
1128
+ return instance
1129
+ }
1130
+ }
1131
+
1132
+ // NOTE (R4-b): the former process-global `_globalManager` + `getWillStem()`
1133
+ // lazy singleton was removed. `WillStem` is instantiated directly — the dev
1134
+ // runner makes its own (`new WillStem()`), and API servers that manage multiple
1135
+ // Wills already do the same. Removing the ambient singleton keeps per-process
1136
+ // state from leaking across Wills/tenants and across test files.
1137
+
1138
+ // ── Utils ─────────────────────────────────────────────────────
1139
+
1140
+ function _sleep( ms: number ): Promise<void> {
1141
+ return new Promise( r => setTimeout( r, ms ) )
1142
+ }
1143
+
1144
+ /** Build a terse emotional state string from a snapshot for conversation context. */
1145
+