@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,167 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/outbox.controller.ts — per-Will messaging / outbox subsystem
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // OutboxController owns the outbound-message machinery extracted from
6
+ // WillStem (R5-c): draining / peeking / re-queuing the per-Will outbox,
7
+ // the message-delivery receipt (reafference percept), and TTL expiry of
8
+ // stale messages. WillStem delegates drainOutbox/peekOutbox/
9
+ // requeueToOutbox/confirmMessageDelivery here and calls expireStale()
10
+ // from its tick loop, so the god-object no longer owns this concern.
11
+ //
12
+ // The outbox ops touch only WillInstance fields (no Will id needed), so
13
+ // these methods take the resolved instance directly. WillStem still
14
+ // validates existence via _get(id) before delegating.
15
+ //
16
+ // OUTBOX_TTL_TICKS lives here (source of truth) and is re-exported from
17
+ // #stem/index for backward-compatible public-API consumers.
18
+ //
19
+ // Behaviour is preserved verbatim from the original WillStem methods;
20
+ // this is a pure extract-collaborator refactor.
21
+ // ─────────────────────────────────────────────────────────────
22
+
23
+ import type { OutboxMessage } from '#types'
24
+ import type { WillInstance } from '#stem/index'
25
+
26
+ /**
27
+ * Number of ticks an undelivered outbox message survives before it is
28
+ * expired. Override via WILL_OUTBOX_TTL_TICKS. Default: 100 ticks.
29
+ */
30
+ export const OUTBOX_TTL_TICKS = parseInt( process.env.WILL_OUTBOX_TTL_TICKS ?? '100')
31
+
32
+ export class OutboxController {
33
+ /**
34
+ * Drain all queued outbound messages from the Will's outbox.
35
+ * Called by the SSE/webhook delivery layer to retrieve messages.
36
+ * Stamps each message with the current tick if createdAtTick is 0.
37
+ */
38
+ drain( instance: WillInstance ): OutboxMessage[] {
39
+ const messages = instance.outbox.splice( 0 )
40
+
41
+ for( const msg of messages )
42
+ if( msg.createdAtTick === 0 ) msg.createdAtTick = instance.tickCount
43
+
44
+ return messages
45
+ }
46
+
47
+ /** Peek at outbox without draining it. */
48
+ peek( instance: WillInstance ): readonly OutboxMessage[] {
49
+ return instance.outbox
50
+ }
51
+
52
+ /**
53
+ * Re-queue messages that were drained but not successfully delivered.
54
+ * Prepends them so they are the first to be drained on the next tick.
55
+ */
56
+ requeue( instance: WillInstance, messages: OutboxMessage[] ): void {
57
+ if( messages.length === 0 ) return
58
+ instance.outbox.unshift( ...messages )
59
+ }
60
+
61
+ /**
62
+ * Called by the delivery layer (SSE consumer or webhook) after a message
63
+ * has been confirmed received by the target entity.
64
+ *
65
+ * Writes a message.delivery percept so the mind perceives its own words
66
+ * landing — the "ear hears the word you spoke" reafference signal.
67
+ */
68
+ confirmDelivery( instance: WillInstance, messageId: string, delivered: boolean ): void {
69
+ const tick = instance.tickCount
70
+
71
+ // 1. Update the conversation.sent entity that tracks this outbox message.
72
+ // ProactiveCommunicator stores outboxMessageIds in the entity metadata (11.3)
73
+ // so we can correlate messageId → sent entity without extra bookkeeping.
74
+ // Use the O(1) type index rather than scanning every entity.
75
+ for( const entity of instance.simulation.stateManager.getEntitiesByType('conversation.sent') ){
76
+ const ids = entity.metadata?.outboxMessageIds as string[] | undefined
77
+ if( !ids?.includes( messageId ) ) continue
78
+
79
+ instance.simulation.stateManager.setEntity({
80
+ id: entity.id,
81
+ type: 'conversation.sent',
82
+ createdAt: entity.createdAt,
83
+ updatedAt: Date.now(),
84
+ metadata: {
85
+ ...entity.metadata,
86
+ delivered,
87
+ deliveredAt: tick,
88
+ },
89
+ })
90
+
91
+ break
92
+ }
93
+
94
+ // 2. Write a percept so Exteroception perceives delivery ("ear hears the word")
95
+ instance.simulation.stateManager.setEntity({
96
+ id: `msg-delivered-${messageId}`,
97
+ type: 'percept',
98
+ createdAt: Date.now(),
99
+ updatedAt: Date.now(),
100
+ metadata: {
101
+ category: 'message-delivery',
102
+ summary: delivered
103
+ ? `Your message was delivered successfully.`
104
+ : `Your message failed to reach the recipient.`,
105
+ salience: delivered ? 0.35 : 0.6,
106
+ changeType: delivered ? 'delivered' : 'failed',
107
+ messageId,
108
+ },
109
+ })
110
+
111
+ instance.sessionLogger?.write({
112
+ type: 'conversation.delivery',
113
+ tick,
114
+ messageId,
115
+ delivered,
116
+ } as any)
117
+ }
118
+
119
+ /**
120
+ * Expire stale outbox messages. Called from the Will tick loop each tick.
121
+ *
122
+ * Messages created with createdAtTick=0 (OutboxWriter default) have not yet been
123
+ * stamped with a real tick — stamp them now so the TTL clock starts here rather
124
+ * than instantly expiring them (age = tickCount - 0 would be >> TTL).
125
+ * Mutate in-place (splice, not filter) so the shared outbox array (held by
126
+ * OutboxWriter) continues to reference the same object after TTL cleanup.
127
+ * Array.filter() would create a new array and silently detach the shared ref.
128
+ */
129
+ expireStale( instance: WillInstance ): void {
130
+ const expiredMessages: OutboxMessage[] = []
131
+
132
+ for( let i = instance.outbox.length - 1; i >= 0; i-- ){
133
+ const msg = instance.outbox[ i ]!
134
+ if( msg.createdAtTick === 0 ) msg.createdAtTick = instance.tickCount
135
+
136
+ const age = instance.tickCount - msg.createdAtTick
137
+ if( age > OUTBOX_TTL_TICKS ){
138
+ expiredMessages.push( msg )
139
+ instance.outbox.splice( i, 1 )
140
+ }
141
+ }
142
+
143
+ for( const msg of expiredMessages ){
144
+ instance.sessionLogger?.write({
145
+ type: 'outbox.expire',
146
+ tick: instance.tickCount,
147
+ messageId: msg.id,
148
+ targetEntityId: msg.targetEntityId,
149
+ effectorName: msg.effectorName,
150
+ ageAtExpiry: instance.tickCount - msg.createdAtTick,
151
+ })
152
+
153
+ try {
154
+ instance.simulation.eventBus.publish(
155
+ {
156
+ type: 'communication.outbound.undelivered',
157
+ source: 'will-manager',
158
+ payload: { messageId: msg.id, targetEntityId: msg.targetEntityId, effectorName: msg.effectorName },
159
+ },
160
+ instance.simulation.context,
161
+ instance.tickCount as any,
162
+ )
163
+ }
164
+ catch { /* event bus may not be flushing — non-critical */ }
165
+ }
166
+ }
167
+ }
@@ -0,0 +1,133 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/outbox.writer.ts — the outbox PRODUCER primitive
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // OutboxWriter is the producer half of the outbox, paired with OutboxController
6
+ // (the consumer half: drain / expire / confirm). It owns the ONE canonical
7
+ // outbox-row shape (id format, default fields) and the `conversation.out`
8
+ // session-audit log for facet replies. Everything that wants to put words on the
9
+ // wire goes through here, so the row shape lives in exactly one place.
10
+ //
11
+ // It is a leaf primitive — constructed with the shared `outbox` array (no
12
+ // WillInstance) and injected into both producers:
13
+ // - ProactiveCommunicator → enqueue() (effector-dispatch path)
14
+ // - AuditionEngine → enqueueReply() (facet-reply fast-path)
15
+ //
16
+ // There is NO gating here. Emission is authorized upstream by AccessGrants
17
+ // (the `talk`/`text`/… grants), checked by the calling engine.
18
+ // ─────────────────────────────────────────────────────────────
19
+
20
+ import { logger } from '#core/logger'
21
+ import type { OutboxMessage } from '#types'
22
+ import type { SessionLogger } from './session.logger'
23
+
24
+ /** The caller-supplied fields of an outbox row; the writer stamps id + defaults. */
25
+ export interface OutboxRow {
26
+ targetEntityId: string
27
+ content: string
28
+ effectorName: OutboxMessage[ 'effectorName' ]
29
+ targetEntityName?: string
30
+ gestureType?: string
31
+ replyToMessageId?: string
32
+ threadId?: string
33
+ }
34
+
35
+ export class OutboxWriter {
36
+ private _outbox: OutboxMessage[]
37
+ private _willId: string
38
+ private _sessionLogger: SessionLogger | null = null
39
+ /**
40
+ * Per-Will monotonic id counter. Deterministic for replay: the writer is
41
+ * created once per Will and `enqueue` is called in the same order on a
42
+ * re-execution, so the ids reproduce exactly (vs the old Date.now()+random,
43
+ * which made the embedded ids in `conversation.sent` diverge every run).
44
+ */
45
+ private _seq = 0
46
+
47
+ constructor( opts: { outbox?: OutboxMessage[]; willId?: string } = {} ){
48
+ this._outbox = opts.outbox ?? []
49
+ this._willId = opts.willId ?? 'will'
50
+ }
51
+
52
+ attachSessionLogger( logger: SessionLogger | null ): void {
53
+ this._sessionLogger = logger
54
+ }
55
+
56
+ private _genId( suffix = '' ): string {
57
+ return `outbox-${ this._willId }-${ ++this._seq }${ suffix }`
58
+ }
59
+
60
+ /**
61
+ * Push one canonical outbox row and return its generated id. The single point
62
+ * where the row shape is materialized — all producers funnel through here.
63
+ */
64
+ enqueue( row: OutboxRow, idSuffix = '' ): string {
65
+ const id = this._genId( idSuffix )
66
+ this._outbox.push({
67
+ id,
68
+ targetEntityId: row.targetEntityId,
69
+ ...( row.targetEntityName !== undefined ? { targetEntityName: row.targetEntityName } : {} ),
70
+ content: row.content,
71
+ effectorName: row.effectorName,
72
+ ...( row.gestureType ? { gestureType: row.gestureType } : {} ),
73
+ ...( row.replyToMessageId ? { replyToMessageId: row.replyToMessageId } : {} ),
74
+ ...( row.threadId ? { threadId: row.threadId } : {} ),
75
+ deliveryStatus: 'pending',
76
+ createdAtTick: 0,
77
+ createdAt: Date.now(),
78
+ })
79
+ return id
80
+ }
81
+
82
+ /**
83
+ * Reply convenience for AuditionEngine (formerly ProactiveCommunicator.deliverReply).
84
+ *
85
+ * Pushes the facet's reply bubbles as `text` outbox rows and writes the
86
+ * `conversation.out` audit entry. Returns the generated message ids (always
87
+ * generated, for delivery correlation), or an empty array when there are no
88
+ * bubbles.
89
+ *
90
+ * `pushToOutbox` (default true) controls only whether a copy is queued: set
91
+ * false when an ExternalTransport already delivered the reply via the fast-path
92
+ * (avoids double delivery) — the ids are still generated and returned.
93
+ */
94
+ enqueueReply( opts: {
95
+ entityId: string
96
+ entityName: string
97
+ bubbles: string[]
98
+ threadId?: string
99
+ tick?: number
100
+ pushToOutbox?: boolean
101
+ } ): string[] {
102
+ const { entityId, entityName, bubbles, threadId, tick = 0, pushToOutbox = true } = opts
103
+
104
+ if( bubbles.length === 0 ) return []
105
+
106
+ const ids: string[] = []
107
+ bubbles.forEach( ( bubble, i ) => {
108
+ if( !pushToOutbox ){ ids.push( this._genId( `-${ i }` ) ); return }
109
+ logger.info(`[outbox-writer] reply → ${ entityId } bubble[${ i }] "${ bubble.slice( 0, 80 ) }"`)
110
+ ids.push( this.enqueue({
111
+ targetEntityId: entityId,
112
+ targetEntityName: entityName,
113
+ content: bubble,
114
+ effectorName: 'text',
115
+ threadId,
116
+ }, `-${ i }` ) )
117
+ } )
118
+
119
+ this._sessionLogger?.write({
120
+ type: 'conversation.out',
121
+ tick,
122
+ targetEntityId: entityId,
123
+ targetEntityName: entityName,
124
+ messageCount: bubbles.length,
125
+ messages: bubbles.map( b => b.slice( 0, 300 ) ),
126
+ preview: bubbles[0]?.slice( 0, 100 ) ?? '',
127
+ threadId,
128
+ source: 'audition-facet',
129
+ })
130
+
131
+ return ids
132
+ }
133
+ }
@@ -0,0 +1,109 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/pma.controller.ts — per-Will PMA (portable mind archive)
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // PMAController owns the PMA subsystem extracted from WillStem (R5-b):
6
+ // the distiller (state → portable snapshot), the loader (snapshot →
7
+ // seeded Will), and the reconstruction-fidelity eval harness. WillStem
8
+ // delegates its loadPMA/distillPMA/runPMAEval methods here, resolving the
9
+ // WillInstance (and validating existence) before each call.
10
+ //
11
+ // Behaviour is preserved verbatim from the original WillStem methods;
12
+ // this is a pure extract-collaborator refactor.
13
+ // ─────────────────────────────────────────────────────────────
14
+
15
+ import { PMADistiller, PMALoader, type PMASnapshot } from '../../pma'
16
+ import { PMAEvalHarness, type ReconstructionFidelityReport } from '../../pma/eval'
17
+ import { validateWillIdentity } from '../guards/identity.guard'
18
+ import { logger } from '#core/logger'
19
+ import type { WillInstance } from '#stem/index'
20
+
21
+ export class PMAController {
22
+ private readonly _distiller = new PMADistiller()
23
+ private readonly _loader = new PMALoader()
24
+ private readonly _evalHarness = new PMAEvalHarness()
25
+
26
+ /**
27
+ * Load a PMASnapshot into a paused or freshly-created Will. The Will must
28
+ * not be active (status 'paused' or 'initializing'); seeding an active Will
29
+ * with existing beliefs may cause unintended merges.
30
+ */
31
+ load( id: string, instance: WillInstance, pma: PMASnapshot ): void {
32
+ if( instance.status === 'active' )
33
+ throw new Error(`Cannot load PMA into an active Will (${id}). Pause first.`)
34
+
35
+ // Re-validate the artifact's persona at the load boundary (Phase 2): a stored
36
+ // or tampered PMA must not inject a collapsed / colliding / injected self on
37
+ // reload, the same way creation guards the operator-supplied identity.
38
+ const guard = validateWillIdentity({ identity: {
39
+ prompt: pma.identity.prompt,
40
+ values: pma.identity.values,
41
+ traits: pma.identity.traits,
42
+ style: pma.identity.style,
43
+ } })
44
+ if( !guard.ok )
45
+ throw new Error(`Invalid PMA identity for "${id}": ${ guard.errors.join('; ') }`)
46
+ for( const w of guard.warnings )
47
+ logger.warn(`[identity-guard] PMA ${id}: ${w}`)
48
+
49
+ const safePma: PMASnapshot = { ...pma, identity: { ...pma.identity, ...guard.sanitized.identity } }
50
+
51
+ this._loader.load(
52
+ safePma,
53
+ instance.simulation,
54
+ instance.cognition,
55
+ )
56
+ }
57
+
58
+ /**
59
+ * Distill the current state of a Will into a portable PMASnapshot. Reads
60
+ * simulation state and JSONL profile logs. Does NOT pause or modify the
61
+ * Will — safe to call while active.
62
+ */
63
+ distill( id: string, instance: WillInstance ): PMASnapshot {
64
+ const
65
+ state = instance.simulation.stateManager.snapshot(),
66
+ sessionId = instance.sessionLogger?.sessionId ?? 'unknown',
67
+ dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
68
+
69
+ return this._distiller.distill(
70
+ id,
71
+ instance.config.name ?? id,
72
+ state,
73
+ sessionId,
74
+ dataDir,
75
+ instance.cognition.schemaRepertoire,
76
+ )
77
+ }
78
+
79
+ /**
80
+ * Distill the Will and score how faithfully the PMA reconstructs it.
81
+ * `vsOriginal` (with `behavioral`) captures the original's probe baseline
82
+ * before evaluating, for reconstruction-vs-original behavioral fidelity.
83
+ *
84
+ * ⚠️ Not read-only when `behavioral` is set: the probe phase runs probes against
85
+ * the LIVE Will, perturbing its state. Don't call it on a production Will you
86
+ * need pristine — distil + eval a paused clone instead.
87
+ */
88
+ async runEval(
89
+ id: string,
90
+ instance: WillInstance,
91
+ opts: { behavioral?: boolean; vsOriginal?: boolean } = {}
92
+ ): Promise<ReconstructionFidelityReport> {
93
+ // Distil the CLEAN state first — capturing the baseline below probes (and
94
+ // therefore mutates) the live original, so order matters.
95
+ const pma = this.distill( id, instance )
96
+
97
+ let baselineDist: Record<string, Record<string, number>> | undefined
98
+ if( opts.behavioral && opts.vsOriginal )
99
+ baselineDist = await this._evalHarness.captureProbeBaseline(
100
+ instance.simulation,
101
+ instance.cognition,
102
+ )
103
+
104
+ return this._evalHarness.evaluate( pma, instance.config, {
105
+ runBehavioralProbes: opts.behavioral ?? false,
106
+ baselineDist,
107
+ })
108
+ }
109
+ }
@@ -0,0 +1,177 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/stem/tracts/replay.controller.ts — per-Will record/replay subsystem
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // ReplayController owns the record/replay machinery extracted from
6
+ // WillStem (R5-a): the ReplayManager, the set of active recorders, the
7
+ // completed-replay metadata cache, and the on-disk replay paths — all
8
+ // keyed by Will id. WillStem delegates its start/stop/getMeta/list/compare
9
+ // replay methods here, so the god-object no longer owns this concern.
10
+ //
11
+ // Behaviour is preserved verbatim from the original WillStem methods;
12
+ // this is a pure extract-collaborator refactor.
13
+ // ─────────────────────────────────────────────────────────────
14
+
15
+ import { mkdirSync, existsSync, readFileSync, readdirSync } from 'node:fs'
16
+ import { join } from 'node:path'
17
+ import {
18
+ ReplayManager,
19
+ type DefaultReplayRecorder,
20
+ type ReplayMetadata,
21
+ type ReplayComparison,
22
+ } from '#core/replay'
23
+ import { setCompletionRecorder, clearCompletionRecorder } from '#core/completion.recorder'
24
+ import { setInboundRecorder, clearInboundRecorder } from '#core/inbound.recorder'
25
+ import type { WillInstance } from '#stem/index'
26
+
27
+ export class ReplayController {
28
+ private readonly _replayManager = new ReplayManager()
29
+ private readonly _activeRecorders = new Map<string, { runId: string; unsub: () => void; recorder: DefaultReplayRecorder }>()
30
+ private readonly _completedReplays = new Map<string, ReplayMetadata>()
31
+ private readonly _replayPaths = new Map<string, string>()
32
+
33
+ /**
34
+ * Begin recording the given Will's ticks (and LLM completions) into a new
35
+ * replay run. Returns the run id. The caller is responsible for resolving
36
+ * `instance` (and thereby validating the Will exists) before calling.
37
+ */
38
+ start( id: string, instance: WillInstance ): string {
39
+ if( this._activeRecorders.has( id ) )
40
+ throw new Error(`Replay already active for Will: ${id}`)
41
+
42
+ const runId = `replay-${Date.now()}-${Math.random().toString(36).slice( 2, 8 )}`
43
+
44
+ // Pre-create the replay dir so flush() can persist segments incrementally
45
+ // (stop() saves the consolidated file into the same dir).
46
+ const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
47
+ const dir = join( dataDir, 'wills', id, 'replays' )
48
+ mkdirSync( dir, { recursive: true } )
49
+
50
+ const recorder = this._replayManager.createRecorder( id, runId, undefined, {
51
+ flushBasePath: join( dir, runId ),
52
+ })
53
+
54
+ const unsub = ( instance.simulation.orchestrator as any ).onAfterTick(
55
+ ( tick: number ) => recorder.recordTick( tick, [] )
56
+ )
57
+
58
+ // Route this Will's LLM completions + external inbound into the recorder
59
+ // while recording is active (both are non-deterministic inputs for replay).
60
+ setCompletionRecorder( id, recorder )
61
+ setInboundRecorder( id, recorder )
62
+
63
+ this._activeRecorders.set( id, { runId, unsub, recorder } )
64
+ return runId
65
+ }
66
+
67
+ async stop( id: string ): Promise<ReplayMetadata> {
68
+ const active = this._activeRecorders.get( id )
69
+ if( !active ) throw new Error(`No active replay for Will: ${id}`)
70
+
71
+ active.unsub()
72
+ clearCompletionRecorder( id )
73
+ clearInboundRecorder( id )
74
+ this._activeRecorders.delete( id )
75
+
76
+ const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
77
+ const dir = join( dataDir, 'wills', id, 'replays' )
78
+ mkdirSync( dir, { recursive: true } )
79
+
80
+ const path = join( dir, `${active.runId}.json` )
81
+ await active.recorder.save( path )
82
+
83
+ const meta = active.recorder.getMetadata()
84
+ const key = `${id}:${active.runId}`
85
+ this._completedReplays.set( key, meta )
86
+ this._replayPaths.set( key, path )
87
+
88
+ return meta
89
+ }
90
+
91
+ getMeta( id: string, runId: string ): ReplayMetadata | null {
92
+ const active = this._activeRecorders.get( id )
93
+ if( active?.runId === runId ) return active.recorder.getMetadata()
94
+
95
+ const key = `${id}:${runId}`
96
+ const cached = this._completedReplays.get( key )
97
+ if( cached ) return cached
98
+
99
+ const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
100
+ const path = join( dataDir, 'wills', id, 'replays', `${runId}.json` )
101
+ if( !existsSync( path ) ) return null
102
+
103
+ try {
104
+ const meta = ( JSON.parse( readFileSync( path, 'utf8' ) ) as { metadata: ReplayMetadata } ).metadata
105
+ this._completedReplays.set( key, meta )
106
+ this._replayPaths.set( key, path )
107
+ return meta
108
+ } catch {
109
+ return null
110
+ }
111
+ }
112
+
113
+ list( id: string ): ReplayMetadata[] {
114
+ const results: ReplayMetadata[] = []
115
+ const prefix = `${id}:`
116
+
117
+ const active = this._activeRecorders.get( id )
118
+ if( active ) results.push( active.recorder.getMetadata() )
119
+
120
+ for( const [ key, meta ] of this._completedReplays )
121
+ if( key.startsWith( prefix ) ) results.push( meta )
122
+
123
+ const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
124
+ const dir = join( dataDir, 'wills', id, 'replays' )
125
+
126
+ if( existsSync( dir ) ){
127
+ const inMem = new Set(
128
+ [ ...this._completedReplays.keys() ]
129
+ .filter( k => k.startsWith( prefix ) )
130
+ .map( k => k.slice( prefix.length ) )
131
+ )
132
+ if( active ) inMem.add( active.runId )
133
+
134
+ for( const file of readdirSync( dir ) ){
135
+ if( !file.endsWith('.json') ) continue
136
+ const runId = file.slice( 0, -5 )
137
+ if( inMem.has( runId ) ) continue
138
+ try {
139
+ const path = join( dir, file )
140
+ const meta = ( JSON.parse( readFileSync( path, 'utf8' ) ) as { metadata: ReplayMetadata } ).metadata
141
+ const key = `${id}:${runId}`
142
+ this._completedReplays.set( key, meta )
143
+ this._replayPaths.set( key, path )
144
+ results.push( meta )
145
+ } catch { /* skip malformed */ }
146
+ }
147
+ }
148
+
149
+ return results.sort( ( a, b ) => b.startTime - a.startTime )
150
+ }
151
+
152
+ async compare( id: string, runId1: string, runId2: string ): Promise<ReplayComparison> {
153
+ const path1 = this._resolvePath( id, runId1 )
154
+ const path2 = this._resolvePath( id, runId2 )
155
+ if( !path1 ) throw Object.assign( new Error(`Replay not found: ${runId1}`), { code: 404 } )
156
+ if( !path2 ) throw Object.assign( new Error(`Replay not found: ${runId2}`), { code: 404 } )
157
+ return this._replayManager.compare( path1, path2 )
158
+ }
159
+
160
+ /**
161
+ * Resolve a replay file path: prefer the in-memory cache, else fall back to the
162
+ * conventional on-disk location. Without the fallback, compare() would 404 for
163
+ * replays that exist on disk but weren't loaded via list()/getMeta() this
164
+ * process (e.g. after a server restart).
165
+ */
166
+ private _resolvePath( id: string, runId: string ): string | undefined {
167
+ const cached = this._replayPaths.get( `${id}:${runId}` )
168
+ if( cached ) return cached
169
+
170
+ const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
171
+ const path = join( dataDir, 'wills', id, 'replays', `${runId}.json` )
172
+ if( !existsSync( path ) ) return undefined
173
+
174
+ this._replayPaths.set( `${id}:${runId}`, path )
175
+ return path
176
+ }
177
+ }