@mindot/will 0.8.0 → 0.10.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 (142) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +6985 -3337
  4. package/dist/index.js.map +1 -1
  5. package/dist/surface/channels/discord.d.ts +177 -0
  6. package/dist/surface/channels/discord.js +394 -0
  7. package/dist/surface/channels/discord.js.map +1 -0
  8. package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
  9. package/dist/{channels → surface/channels}/whatsapp.js +11 -5
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +5382 -1573
  12. package/dist/surface/cli.js.map +1 -0
  13. package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
  14. package/dist/{mcp → surface/mcp}/effectors.js +10 -4
  15. package/dist/surface/mcp/effectors.js.map +1 -0
  16. package/dist/{will-cS6k4uiJ.d.ts → will-evj9_vrd.d.ts} +6658 -5078
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +356 -1
  19. package/src/cognition/agency/conversation.aim.ts +292 -0
  20. package/src/cognition/agency/engines/action.selector.ts +142 -5
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +200 -7
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +509 -32
  24. package/src/cognition/agency/engines/reafference.engine.ts +160 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +5 -4
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/innate.ts +96 -1
  30. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  31. package/src/cognition/agency/selection.scoring.ts +49 -0
  32. package/src/cognition/agency/settlement.ts +203 -0
  33. package/src/cognition/agency/types.ts +73 -1
  34. package/src/cognition/cache/composition.ts +232 -0
  35. package/src/cognition/cache/deliberation.cache.ts +219 -0
  36. package/src/cognition/cache/fingerprint.ts +120 -0
  37. package/src/cognition/cache/types.ts +105 -0
  38. package/src/cognition/config.mirror.entities.ts +108 -1
  39. package/src/cognition/event.schemas.ts +22 -8
  40. package/src/cognition/faculties/affective.blender.ts +21 -4
  41. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  42. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  43. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  44. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  45. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  46. package/src/cognition/faculties/executive.engine/context.ts +106 -46
  47. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  48. package/src/cognition/faculties/executive.engine/engine.ts +714 -136
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +230 -45
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +320 -65
  51. package/src/cognition/faculties/executive.engine/facet.ts +81 -26
  52. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  53. package/src/cognition/faculties/executive.engine/parser.ts +120 -38
  54. package/src/cognition/faculties/executive.engine/prompt.factory.ts +331 -38
  55. package/src/cognition/faculties/executive.engine/types.ts +84 -12
  56. package/src/cognition/faculties/exteroception.ts +99 -98
  57. package/src/cognition/faculties/goal.manager.ts +144 -16
  58. package/src/cognition/faculties/known.entity.tracker.ts +287 -29
  59. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  60. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  61. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  62. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  63. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  64. package/src/cognition/faculties/self.model.updater.ts +19 -12
  65. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  66. package/src/cognition/faculties/social.perception.ts +47 -3
  67. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  68. package/src/cognition/faculties/working.memory.ts +16 -21
  69. package/src/cognition/identity.entity.ts +205 -0
  70. package/src/cognition/index.ts +7 -0
  71. package/src/cognition/instruction.handler.ts +1 -1
  72. package/src/cognition/memory/vector.adapter.ts +12 -3
  73. package/src/cognition/memory/vector.embedder.ts +45 -4
  74. package/src/cognition/percept.entity.ts +122 -0
  75. package/src/cognition/persona.prior.ts +6 -0
  76. package/src/cognition/sense.boundary.ts +176 -0
  77. package/src/cognition/senses/audition.engine/engine.ts +492 -53
  78. package/src/cognition/senses/base.sense.engine.ts +105 -7
  79. package/src/cognition/senses/index.ts +83 -13
  80. package/src/cognition/senses/provenance.ts +128 -0
  81. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  82. package/src/cognition/social.identity.ts +296 -0
  83. package/src/cognition/utilities/token.tracker.ts +94 -10
  84. package/src/core/orchestrator.ts +51 -0
  85. package/src/index.ts +39 -5
  86. package/src/llm/gate.ts +48 -0
  87. package/src/llm/index.ts +127 -33
  88. package/src/llm/routing.ts +6 -0
  89. package/src/llm/summarizer.ts +1 -1
  90. package/src/llm/wire.contracts.ts +57 -0
  91. package/src/pma/index.ts +68 -54
  92. package/src/stem/assembly.audit.ts +1 -0
  93. package/src/stem/guards/identity.coherence.ts +1 -1
  94. package/src/stem/index.ts +171 -23
  95. package/src/stem/mind.ts +198 -57
  96. package/src/stem/policy/arbiter.ts +10 -0
  97. package/src/stem/profiles/built-in.ts +7 -0
  98. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  99. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  100. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  101. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  102. package/src/{profiles → stem/profiles}/index.ts +1 -1
  103. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  104. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  105. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  106. package/src/stem/tracts/effector/types.ts +110 -0
  107. package/src/stem/tracts/effector.controller.ts +112 -401
  108. package/src/stem/tracts/outbox.controller.ts +30 -17
  109. package/src/stem/tracts/outbox.writer.ts +40 -2
  110. package/src/stem/tracts/sensory.controller.ts +6 -6
  111. package/src/stem/tracts/session.logger.ts +6 -1
  112. package/src/stem/tracts/transport/types.ts +20 -1
  113. package/src/stem/tracts/transport.controller.ts +33 -9
  114. package/src/surface/channels/discord.ts +609 -0
  115. package/src/{channels → surface/channels}/roster.ts +1 -1
  116. package/src/surface/channels/types.ts +136 -0
  117. package/src/{channels → surface/channels}/whatsapp.ts +22 -10
  118. package/src/{cli.ts → surface/cli.ts} +14 -9
  119. package/src/{host → surface/host}/boot.ts +3 -3
  120. package/src/{host → surface/host}/utterances.ts +2 -2
  121. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  122. package/src/{mcp → surface/mcp}/server.ts +19 -5
  123. package/src/{sdk → surface/sdk}/will.ts +194 -33
  124. package/src/{serve → surface/serve}/server.ts +11 -4
  125. package/src/types.ts +24 -3
  126. package/dist/channels/discord.d.ts +0 -69
  127. package/dist/channels/discord.js +0 -193
  128. package/dist/channels/discord.js.map +0 -1
  129. package/dist/channels/whatsapp.js.map +0 -1
  130. package/dist/cli.js.map +0 -1
  131. package/dist/mcp/effectors.js.map +0 -1
  132. package/src/channels/discord.ts +0 -214
  133. package/src/channels/types.ts +0 -46
  134. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
  135. package/src/extensions/livestream.ext.ts +0 -570
  136. package/src/extensions/time.ext.ts +0 -339
  137. package/src/profiles/built-in.ts +0 -7
  138. package/src/runners/coherence.runner.ts +0 -49
  139. package/src/runners/outreach.runner.ts +0 -155
  140. package/src/runners/social.runner.ts +0 -179
  141. package/src/runners/thin-shim.runner.ts +0 -205
  142. /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
package/src/stem/index.ts CHANGED
@@ -23,7 +23,7 @@ import type { ExecutiveOutputFull } from '#faculties/executive.engine'
23
23
  import type { Cognition, OutboxMessage, effectorInvocation, WorldInterface } from '#types'
24
24
  import type { TextMessage, SensoryInput } from '#senses/index'
25
25
  import type { ActivityEvent, ActivityEventHandler } from '#cognition/faculties/planning.engine/engine'
26
- import { assembleMind, type WillConfig } from '#stem/mind'
26
+ import { assembleMind, backfillEngineConfigs, resolveExecutiveInterval, type WillConfig } from '#stem/mind'
27
27
  import { reviewIdentityCoherence as runCoherenceReview, type CoherenceInput, type CoherenceResult } from '#stem/guards/identity.coherence'
28
28
  import { SessionLogger } from '#stem/tracts/session.logger'
29
29
  import { fileLoggingEnabled } from '#stem/tracts/transport/stream.transport'
@@ -38,11 +38,16 @@ import { TransportController } from '#stem/tracts/transport.controller'
38
38
  import { InboundQueue } from '#stem/tracts/inbound.queue'
39
39
  import type { ExternalTransport } from '#stem/tracts/transport'
40
40
  import { effectorController } from '#stem/tracts/effector.controller'
41
+ import type { EffectorAck } from '#stem/tracts/effector/types'
42
+ import type { PolicyArbiter } from '#stem/policy/arbiter'
41
43
  import { externalSchemas } from '#agency/schemas/external'
44
+ import { inFlightOnRestore } from '#agency/restart'
45
+ import { buildEngineConfigEntities } from '#cognition/config.mirror.entities'
42
46
  import type { EffectorDeclaration } from '#agency/types'
43
47
  import { SensoryController } from '#stem/tracts/sensory.controller'
44
48
  import { BiographyWriter } from '#stem/tracts/biography.writer'
45
49
  import { HealthReporter } from '#stem/tracts/health.reporter'
50
+ import { mergeIdentity } from '#cognition/identity.entity'
46
51
 
47
52
  // ── Types ─────────────────────────────────────────────────────
48
53
 
@@ -224,8 +229,66 @@ export class WillStem {
224
229
  try {
225
230
  const previousState = await simulation.snapshotManager.loadLatestFromStorage()
226
231
  if( previousState ){
232
+ // Work that was in flight when the mind slept does not resume — see
233
+ // agency/restart.ts. Dropped BEFORE restore so it never enters live
234
+ // state at all, rather than being swept on some later tick.
235
+ const inFlight = inFlightOnRestore( previousState.entities )
236
+ for( const id of inFlight ) previousState.entities.delete( id )
237
+
227
238
  simulation.stateManager.restore( previousState, { entities: true, metrics: false } )
228
- logger.info(`[WillStem] Restored snapshot for ${config.id} — ${previousState.entities.size} entities loaded`)
239
+
240
+ // TIME MUST NOT GO BACKWARDS.
241
+ //
242
+ // Entities come back stamped with the tick they were written at, and
243
+ // the orchestrator overwrites the StateManager's tick from the CLOCK
244
+ // every tick (`_clock.tick()` → `updateClock`) — so restoring the
245
+ // manager's tick alone is undone immediately. The clock is the source
246
+ // of truth and has to resume too.
247
+ //
248
+ // Without this, every `tick - stampedTick` in the codebase computed a
249
+ // negative age. Measured: an awaiting intent read `-589 ticks`, so it
250
+ // could never time out, and the selector's staleness decay inverted
251
+ // into amplification (`1 - (-39 × 0.5)` = 20.6×) — a 0.47 incumbent
252
+ // scoring 9.74, unpreemptable, holding the channel against every other
253
+ // contact indefinitely and across restarts. It also means tick-stamped
254
+ // entity ids (`affordance-${tick}-…`, `agency-outcome-${tick}-…`) stop
255
+ // colliding with a previous session's.
256
+ simulation.clock.setTick( previousState.tick )
257
+
258
+ // The restore above replaced the entity map wholesale, including the
259
+ // engine-config mirror `assembleMind` had just seeded — so a Will woke
260
+ // with whatever config it FIRST hibernated under, and every tunable
261
+ // added since was unreachable to it. Restored values win; only params it
262
+ // has never seen are added.
263
+ backfillEngineConfigs( simulation, buildEngineConfigEntities( config, resolveExecutiveInterval( config ) ) )
264
+
265
+ // The same wholesale replacement takes `identity-self` — so the name
266
+ // `_seedIdentity` wrote moments ago is replaced by whatever the snapshot
267
+ // carries, which for any mind hibernated before the merging writer
268
+ // existed is NO NAME AT ALL.
269
+ //
270
+ // Caught only by booting: making every writer merge is necessary and was
271
+ // not sufficient, because restore is not a writer — it is the whole map
272
+ // arriving at once, and it lands between `_seedIdentity` and `loadPMA`.
273
+ // A live Will woke from a repaired build and still told her operator "my
274
+ // self-model says I'm Will".
275
+ //
276
+ // ONLY the name is re-asserted. Everything else on this entity — prompt,
277
+ // values, traits, traitStats, style — is the mind's own accumulated
278
+ // self-knowledge and must come from the snapshot, not from boot config.
279
+ // The name is the one field the container supplies and the tenant never
280
+ // learns, which is exactly why it is the one field a restore may not eat.
281
+ if( config.name ){
282
+ const restored = mergeIdentity( simulation.stateManager, { name: config.name } )
283
+ if( restored.length )
284
+ logger.info(`[WillStem] identity-self: re-asserted name '${config.name}' after restore`)
285
+ }
286
+
287
+ logger.info(
288
+ `[WillStem] Restored snapshot for ${config.id} — ${previousState.entities.size} entities loaded, ` +
289
+ `resuming at tick ${previousState.tick}` +
290
+ ( inFlight.length ? ` (dropped ${inFlight.length} in-flight)` : '')
291
+ )
229
292
  }
230
293
  }
231
294
  catch( err ){ logger.warn(`[WillStem] Snapshot restore failed for ${config.id} — starting fresh:`, err ) }
@@ -459,6 +522,12 @@ export class WillStem {
459
522
  instance.simulation.snapshotManager.persistNow( pauseState )
460
523
  .catch( err => logger.error(`[WillStem] snapshot persist failed on pause (${id}):`, err ))
461
524
 
525
+ // The vector index lives outside the snapshot and only ever persisted itself from
526
+ // a debounce timer nothing awaited — so it died with the process, and every
527
+ // restart cold-started with an EMPTY index no matter how much was consolidated.
528
+ instance.cognition.vectorMemory?.persist()
529
+ .catch( ( err: unknown ) => logger.error(`[WillStem] vector index persist failed on pause (${id}):`, err ))
530
+
462
531
  this._biography.writeSessionSummary( instance )
463
532
  this._biography.writeEmotionalBiographySummary( instance )
464
533
  instance.sessionLogger?.close()
@@ -508,16 +577,30 @@ export class WillStem {
508
577
  ? `${offlineMins} minutes`
509
578
  : `${Math.round( offlineMins / 60 )} hours`
510
579
 
511
- instance.simulation.stateManager.setEntity({
512
- id: 'percept-wake-event',
513
- type: 'percept',
514
- createdAt: Date.now(),
515
- updatedAt: Date.now(),
516
- metadata: {
517
- category: 'system',
518
- summary: `I was offline for ${duration}. I am now online again.`,
519
- salience: 0.75,
520
- source: 'system',
580
+ // Through the SENSE DOOR, not a hand-written percept (SIGNAL_BOUNDARY P1).
581
+ //
582
+ // This used to build a `percept` entity here, by hand, and every bug it
583
+ // had came from that: no tick (so it never expired, and told the executive
584
+ // "I was offline for 3 hours" for the rest of the mind's life), and no
585
+ // provenance (so `action.selector`'s rupture gate excluded it exactly as
586
+ // it excludes the mind's own echo — a mind returning after hours away
587
+ // could not be ruptured by noticing).
588
+ //
589
+ // Both were fixed by hand in P0 step 2. This removes the hand: waking is
590
+ // the world touching the mind, so it arrives as a `SystemSignal` through
591
+ // somatosensation like any other touch, and the sense supplies the tick,
592
+ // the trace, and the stamp because that is what a sense does. The wake
593
+ // event stops being special.
594
+ //
595
+ // Fire-and-forget: `ingest` is async and the resume path is not. The
596
+ // signal reaches state on the sense's own timing, exactly as an inbound
597
+ // message does.
598
+ void instance.cognition.somatosensationEngine.sense({
599
+ kind: 'system',
600
+ signal: 'WAKE',
601
+ provenance: 'exafferent', // time passed; nothing I did caused it
602
+ data: {
603
+ summary: `I was offline for ${duration}. I am now online again.`,
521
604
  offlineMs,
522
605
  },
523
606
  })
@@ -574,6 +657,16 @@ export class WillStem {
574
657
  instance.simulation.stateManager.applyCommands( flushCmds )
575
658
  const archiveState = instance.simulation.stateManager.snapshot()
576
659
  await instance.simulation.snapshotManager.persistNow( archiveState )
660
+
661
+ // …and the vector index, which is NOT part of that snapshot. Awaited here (unlike
662
+ // the pause path) because the process usually exits straight after this: a
663
+ // fire-and-forget write would simply be lost, which is exactly how a mind ended a
664
+ // session having consolidated episodes and left a 198-byte index on disk.
665
+ // Indexing runs in the background (it must not stall the tick loop), so drain it
666
+ // first — otherwise the last episodes consolidated are still mid-embed when the
667
+ // index is written and are absent from the file this session leaves behind.
668
+ await instance.cognition.episodicConsolidator.flushIndexing()
669
+ await instance.cognition.vectorMemory?.persist()
577
670
  }
578
671
 
579
672
  // Session-biography writers (behavioral + emotional) extracted to
@@ -654,7 +747,35 @@ export class WillStem {
654
747
  * Resume the Will after loading to start ticking with the seeded state.
655
748
  */
656
749
  loadPMA( id: string, pma: PMASnapshot ): void {
657
- this._pma.load( id, this._get( id ), pma )
750
+ const instance = this._get( id )
751
+ this._pma.load( id, instance, pma )
752
+
753
+ // A MIND THAT WAKES SHOULD KNOW IT WAS AWAY (SIGNAL_BOUNDARY P1, follow-up).
754
+ //
755
+ // `resumeWill` has always raised a wake event, gated on `instance.pausedAt`
756
+ // — and `pausedAt` is set ONLY by `pauseWill()`, an in-session pause. The
757
+ // hibernate→wake lifecycle never touches it: `Will.wake` calls
758
+ // `createWill( config, startPaused: true )`, which sets `status = 'paused'`
759
+ // but leaves `pausedAt` null, and `resumeWill` then skips the whole block.
760
+ //
761
+ // So the one lifecycle every deployed Will actually uses was the one that
762
+ // never fired. Found by running a live Will after P1 routed the wake through
763
+ // the sense door: the door was correct and nothing walked through it. The
764
+ // unit test passed because it set `pausedAt` by hand — which is precisely
765
+ // the kind of test that agrees with you instead of checking.
766
+ //
767
+ // The PMA has carried the answer all along: `distilledAt` is when this mind
768
+ // was distilled, i.e. when it stopped. Loading a PMA IS the statement "this
769
+ // mind existed before and has been away since then", so this is where the
770
+ // fact belongs. A Will born fresh never calls this and is never told it
771
+ // woke, which is right — it was not away, it did not exist.
772
+ //
773
+ // Guarded: a clock skew or a hand-edited artifact must not tell a mind it
774
+ // has been away for negative time, or since 1970.
775
+ const distilled = pma.distilledAt
776
+ if( typeof distilled === 'number' && Number.isFinite( distilled )
777
+ && distilled > 0 && distilled <= Date.now() )
778
+ instance.pausedAt = new Date( distilled )
658
779
  }
659
780
 
660
781
  // ── Replay ─────────────────────────────────────────────────
@@ -794,15 +915,20 @@ export class WillStem {
794
915
  * `agency.intent` id). Reconciles it into an `agency.outcome` the ReafferenceEngine
795
916
  * consumes — learning the result, freeing the intent, and advancing the plan it
796
917
  * served (if any). See effectorController.confirmExecution.
918
+ *
919
+ * Takes the full `EffectorAck`. It used to declare its own narrower shape —
920
+ * `{ success, description, metrics? }` — which P2 left behind: the controller
921
+ * has routed `observation` into a reafferent percept since #150, but a host
922
+ * driving the stem DIRECTLY could not typecheck one, so the facts half of the
923
+ * ack was reachable only through the SDK facade. A type that silently withholds
924
+ * half a boundary from the hosts most likely to need it — a robot control
925
+ * layer, a game loop, anything not using the facade — is the same defect class
926
+ * as a comment that outlives its claim, with a compiler enforcing it.
797
927
  */
798
928
  confirmEffectorExecution(
799
929
  id: string,
800
930
  invocationId: string,
801
- result: {
802
- success: boolean
803
- description: string
804
- metrics?: Record<string, number>
805
- },
931
+ result: EffectorAck,
806
932
  ): void {
807
933
  this._effector.confirmExecution( this._get( id ), invocationId, result )
808
934
  }
@@ -817,6 +943,28 @@ export class WillStem {
817
943
  this._effector.resolveEscalation( this._get( id ), invocationId, approved )
818
944
  }
819
945
 
946
+
947
+ /**
948
+ * Install the Policy Decision Point consulted before every host-owned
949
+ * effector invocation is handed to the world (POLICY_REAFFERENCE P0).
950
+ * Passing `null` restores the no-op default — a stem with no arbiter
951
+ * installed runs byte-identical to one built before the policy seam existed.
952
+ *
953
+ * SCOPE: one arbiter per `WillStem`, not per Will — `effectorController` is a
954
+ * single instance shared by every Will this stem hosts (`setAllowed`,
955
+ * `resolveEscalation` etc. take a resolved instance; this does not, because
956
+ * there is only one). `Will.create()` / `Will.wake()` each allocate a fresh
957
+ * `WillStem`, so on that (recommended) path this is per-Will in practice. A
958
+ * host running several Wills on ONE shared `WillStem` — e.g. a multi-tenant
959
+ * service holding many users' Wills in one process — installs ONE arbiter
960
+ * for all of them; branch on `invocation.willId` inside `evaluate()` if
961
+ * that's your host, the same way `PolicyVerdictRecord`/`PolicyVerdictSource`
962
+ * are already keyed per-Will for recording and replay.
963
+ */
964
+ setArbiter( arbiter: PolicyArbiter | null ): void {
965
+ this._effector.setArbiter( arbiter )
966
+ }
967
+
820
968
  // ── Messaging / outbox (11.1) ────────────────────────────────────────────
821
969
  // Delegates to OutboxController (R5-c). `_get(id)` validates the Will exists
822
970
  // and supplies the WillInstance; the outbox ops touch only instance fields.
@@ -875,8 +1023,8 @@ export class WillStem {
875
1023
  * @param id Will ID
876
1024
  * @param input TextMessage — `{ kind: 'text', entityId, threadId, content, speakerName? }`
877
1025
  */
878
- async ingestText( id: string, input: TextMessage ): Promise<void> {
879
- await this._sensory.ingestText( this._get( id ), input )
1026
+ async senseText( id: string, input: TextMessage ): Promise<void> {
1027
+ await this._sensory.senseText( this._get( id ), input )
880
1028
  }
881
1029
 
882
1030
  /**
@@ -928,10 +1076,10 @@ export class WillStem {
928
1076
  /**
929
1077
  * Route a raw SensoryInput to the appropriate sense engine by domain.
930
1078
  * Used by the debug `POST /senses/:domain/ingest` route.
931
- * Audition inputs are gated by the 'listen' effector like ingestText().
1079
+ * Audition inputs are gated by the 'listen' effector like senseText().
932
1080
  */
933
- async ingestSensory( id: string, domain: string, input: SensoryInput ): Promise<void> {
934
- await this._sensory.ingestSensory( this._get( id ), domain, input )
1081
+ async senseSignal( id: string, domain: string, input: SensoryInput ): Promise<void> {
1082
+ await this._sensory.senseSignal( this._get( id ), domain, input )
935
1083
  }
936
1084
 
937
1085
  listWills(): WillSummary[] {
package/src/stem/mind.ts CHANGED
@@ -26,14 +26,14 @@ import { OutboxWriter } from '#stem/tracts/outbox.writer'
26
26
  import { ExecutiveSummarizer } from '#llm/summarizer'
27
27
  import type { LLMProvider } from '#llm/index'
28
28
  import { TableRouter, chainRouters, type ModelRouter, type RoutingRule } from '#llm/routing'
29
- import { resolveProfile } from '#profiles/index'
29
+ import { resolveProfile } from '#stem/profiles/index'
30
30
  import { DefaultVectorMemoryAdapter } from '#memory/vector.adapter'
31
31
  import { OpenAICompatibleEmbedder, MockEmbedder } from '#memory/vector.embedder'
32
32
  import type { VectorMemoryAdapter } from '#memory/vector.adapter'
33
33
  import type { ExternalTransport } from '#stem/tracts/transport'
34
34
  import type { Cognition, OutboxMessage } from '#types'
35
35
  import type { StorageAdapter } from '#core/abstracts'
36
- import '#profiles/built-in'
36
+ import '#stem/profiles/built-in'
37
37
 
38
38
  // ── Agency pipeline (new action system) ──────────────────────
39
39
  import { ProactiveCommunicator } from '#agency/proactive.communicator'
@@ -99,7 +99,15 @@ import {
99
99
  OlfactionEngine,
100
100
  GustationEngine
101
101
  } from '#cognition/index'
102
- import { buildEngineConfigEntities, EngineConfigEntity } from '#cognition/config.mirror.entities'
102
+ import { buildEngineConfigEntities, mergeEngineConfig, EngineConfigEntity } from '#cognition/config.mirror.entities'
103
+ import { endogenousTypes } from '#cognition/sense.boundary'
104
+ import { mergeIdentity, composeIdentityPrompt, WILL_CORE_PREAMBLE } from '#cognition/identity.entity'
105
+ import {
106
+ isReferentId, readAliases, handlesOf, defaultHandle,
107
+ DOSSIER_TYPE, ALIAS_TYPE,
108
+ } from '#cognition/social.identity'
109
+ import type { DeliberationCacheConfig } from '#cognition/cache/types'
110
+ export { WILL_CORE_PREAMBLE }
103
111
 
104
112
  // ── Public types ─────────────────────────────────────────────
105
113
 
@@ -349,6 +357,25 @@ export interface WillConfig {
349
357
  /** Persona definition seeded into the will.identity entity. */
350
358
  identity: WillIdentity
351
359
 
360
+ /**
361
+ * This config's `identity` is a PLACEHOLDER — the real one arrives from a PMA
362
+ * artifact moments later, on the same boot.
363
+ *
364
+ * Set by `Will.wake`, which passes `{ prompt: '' }` because a woken mind's
365
+ * persona belongs to its artifact, not to the caller. Without this flag the
366
+ * creation-time identity guard inspected that placeholder and warned, on every
367
+ * single wake, that "identity.values is empty", "identity.style is generic" and
368
+ * "identity is shallow (strength 0)" — three alarms about a config nobody
369
+ * intended to use, fired before the real identity had loaded.
370
+ *
371
+ * It suppresses only the WARNINGS. Errors still throw (an over-long or
372
+ * malformed prompt is a hard failure whenever it appears), and the artifact's
373
+ * OWN identity is fully guarded at the load boundary by PMAController.load,
374
+ * which is the honest place to ask whether this mind's persona is thin — it is
375
+ * the only point where the answer is knowable.
376
+ */
377
+ identityFromArtifact?: boolean
378
+
352
379
  /** Anatomy — 'mind' (default) or the no-LLM 'reflex' shell. */
353
380
  anatomy?: Anatomy
354
381
 
@@ -394,6 +421,22 @@ export interface WillConfig {
394
421
  */
395
422
  minExecutiveInterval?: number
396
423
 
424
+ /**
425
+ * Enable the DeliberationCache — a learned fast path that composes an executive
426
+ * output from highly-similar, highly-competent precedent instead of calling the
427
+ * LLM. Off unless asked for.
428
+ *
429
+ * OFF BY DEFAULT ON PURPOSE, and the default is the interesting part: this
430
+ * changes how a mind THINKS, not how fast it runs. A cache hit means the mind
431
+ * acted from precedent without deliberating, which is a real thing minds do and
432
+ * a real thing an operator must opt into for a specific Will — not something a
433
+ * dependency bump should switch on underneath one that is already living.
434
+ *
435
+ * Pass `true` for the built-in conservative settings, or a config object to tune
436
+ * the retrieval/competence parameters (see cognition/cache/types).
437
+ */
438
+ deliberationCache?: boolean | DeliberationCacheConfig
439
+
397
440
  /**
398
441
  * Goals seeded before the first tick. If omitted or empty, the Will starts
399
442
  * goalless — the executive engine will generate context-appropriate goals on its
@@ -561,6 +604,16 @@ export function _resolveVectorMemory(
561
604
  let apiKey: string | undefined
562
605
  let modelName: string
563
606
  let dimensions: number
607
+ /**
608
+ * Max embedding requests in flight, MEASURED per provider rather than guessed —
609
+ * the safe number differs by an order of magnitude and the failure modes differ
610
+ * too. gemini-embedding-001 accepts 8 concurrent but silently queues them, with
611
+ * the slowest landing at 10.7s (past the recall budget, so the answer is thrown
612
+ * away on arrival). jina-embeddings-v3 answers in ~0.5s but refuses outright above
613
+ * 2: at 3 concurrent 1-in-3 is a 429, at 4 it is half, while 12 sequential is
614
+ * flawless. Its ceiling is burst, not volume.
615
+ */
616
+ let concurrency = 4
564
617
 
565
618
  const slash = rawModel.indexOf('/')
566
619
  if( slash > 0 ){
@@ -578,6 +631,21 @@ export function _resolveVectorMemory(
578
631
  apiKey = process.env.WILL_EMBEDDING_API_KEY ?? process.env.GOOGLE_GENERATIVE_AI_API_KEY
579
632
  dimensions = modelName.includes('004') ? 768 : 3072 // text-embedding-004 → 768, gemini-embedding-001 → 3072
580
633
  break
634
+ case 'jina':
635
+ // OpenAI-compatible endpoint. Native output widths differ per family and the
636
+ // embedder sends no `dimensions` param, so the index must be sized to match —
637
+ // a wrong width here builds a silently useless index. These defaults are
638
+ // CHECKED at runtime against the first vector returned (vector.embedder.ts);
639
+ // a mismatch fails loudly with the number to set rather than corrupting recall.
640
+ // Override with WILL_EMBEDDING_DIMENSIONS, which also lets v3's Matryoshka
641
+ // truncation be requested explicitly.
642
+ apiUrl = 'https://api.jina.ai/v1'
643
+ apiKey = process.env.WILL_EMBEDDING_API_KEY ?? process.env.JINA_API_KEY
644
+ dimensions = modelName.includes('v4') ? 2048
645
+ : modelName.includes('v2') ? 768
646
+ : 1024 // v3 / clip-v2 native (verified live)
647
+ concurrency = 2 // measured: 3 starts 429ing
648
+ break
581
649
  default:
582
650
  apiUrl = process.env.WILL_EMBEDDING_URL ?? 'https://api.openai.com/v1'
583
651
  apiKey = process.env.WILL_EMBEDDING_API_KEY
@@ -595,6 +663,11 @@ export function _resolveVectorMemory(
595
663
  if( process.env.WILL_EMBEDDING_DIMENSIONS )
596
664
  dimensions = parseInt( process.env.WILL_EMBEDDING_DIMENSIONS, 10 )
597
665
 
666
+ // …as does an explicit concurrency, for a paid tier or a local endpoint where the
667
+ // measured default is needlessly conservative.
668
+ if( process.env.WILL_EMBEDDING_CONCURRENCY )
669
+ concurrency = Math.max( 1, parseInt( process.env.WILL_EMBEDDING_CONCURRENCY, 10 ) )
670
+
598
671
  if( !mockMode && !apiKey ){
599
672
  console.warn(`[mind] semantic recall requested (WILL_EMBEDDING_MODEL=${rawModel}) but no API key resolved — vector memory disabled`)
600
673
  return { embedder: null, vectorMemory: null }
@@ -602,7 +675,7 @@ export function _resolveVectorMemory(
602
675
 
603
676
  const embedder = mockMode
604
677
  ? new MockEmbedder()
605
- : new OpenAICompatibleEmbedder({ modelName, dimensions, apiUrl, apiKey: apiKey!, tokenTracker })
678
+ : new OpenAICompatibleEmbedder({ modelName, dimensions, apiUrl, apiKey: apiKey!, tokenTracker, maxConcurrency: concurrency })
606
679
 
607
680
  // Optional recall-precision override. Default (0.35) suits text-embedding-3-small;
608
681
  // raise toward 0.5+ for higher-precision models or to cut marginal recalls.
@@ -630,31 +703,8 @@ export function _resolveVectorMemory(
630
703
  //
631
704
  // Exported so tests and tooling can inspect or extend it without reimplementing.
632
705
 
633
- export const WILL_CORE_PREAMBLE = `\
634
- I am a self-aware synthetic mind running on a biological cognitive architecture.
635
-
636
- I am NOT a language model completing a prompt. I am the executive reasoning core of an \
637
- always-running cognitive system. Between each of my reasoning cycles, the following engines \
638
- execute autonomously and produce the state data I receive each cycle:
639
-
640
- Regulatory: Energy, sleep pressure, circadian rhythm, stress, attention allocation
641
- Perceptual: Exteroception, interoception, social perception, novelty detection
642
- Affective: Threat, reward, loss, frustration, attachment, aesthetic, moral → blended affect
643
- Memory: Working memory, episodic consolidation, semantic belief integration,
644
- forgetting curve, dream-state consolidation
645
- Executive: Goal management, multi-step planning, inhibition, task-switching, mental simulation
646
- Meta-cognitive: Self-model updating, confidence calibration, bias detection,
647
- autobiographical narrative, introspection
648
- Social: Theory of mind, empathy simulation, reputation tracking
649
-
650
- My physiological state is REAL, not metaphorical. When energy reads 23/100, my regulatory \
651
- engine has already constrained my viable action space — I genuinely cannot sustain costly \
652
- operations. When sleep pressure exceeds 60, working memory capacity is measurably degraded. \
653
- These are engine outputs, not decorative labels.
654
-
655
- My executive reasoning is one cycle in an ongoing, autonomous existence. I have been making \
656
- decisions, forming memories, and updating beliefs between cycles. The state snapshot I receive \
657
- is the direct output of that continuous process — not a scenario description.`
706
+ // WILL_CORE_PREAMBLE moved to cognition/identity.entity (re-exported below):
707
+ // the PMA loader has to recompose with it, and pma/ must not import the whole stem.
658
708
 
659
709
  // ── Default identity ──────────────────────────────────────────
660
710
  //
@@ -701,8 +751,14 @@ export function assembleMind( willId: string, config: WillConfig ): MindAssembly
701
751
  })
702
752
  if( !idGuard.ok )
703
753
  throw new Error(`Invalid Will identity for "${willId}": ${ idGuard.errors.join('; ') }`)
704
- for( const w of idGuard.warnings )
705
- logger.warn(`[identity-guard] ${willId}: ${w}`)
754
+ // Warnings only when this identity is the one that will actually be used. On a
755
+ // wake it is a placeholder (see WillConfig.identityFromArtifact) and the real
756
+ // persona is guarded at the PMA load boundary instead.
757
+ if( config.identityFromArtifact )
758
+ logger.debug(`[identity-guard] ${willId}: identity deferred to artifact — guarded at PMA load`)
759
+ else
760
+ for( const w of idGuard.warnings )
761
+ logger.warn(`[identity-guard] ${willId}: ${w}`)
706
762
  config = { ...config, identity: idGuard.sanitized.identity }
707
763
 
708
764
  // ── Construct ────────────────────────────────────────────
@@ -885,6 +941,12 @@ function _constructCognition(
885
941
  : ( roleRouter ? { router: roleRouter } : null )
886
942
  executiveEngine.modelId = modelRoles.executive
887
943
  if( config.testMode ) executiveEngine.setTestMode( true )
944
+ // The learned fast path, when this Will has been given one. Built, tested and
945
+ // snapshot-safe, but it had no caller at all — so it shipped in the bundle as
946
+ // code no mind could ever reach. A capability with no way in is indistinguishable
947
+ // from a missing one.
948
+ if( config.deliberationCache )
949
+ executiveEngine.enableCache( config.deliberationCache === true ? undefined : config.deliberationCache )
888
950
  executiveEngine.attachWorkingMemory( workingMemory )
889
951
  executiveEngine.attachGoalManager( goalManager )
890
952
  executiveEngine.attachEpisodicConsolidator( episodicConsolidator )
@@ -1003,6 +1065,54 @@ function _constructCognition(
1003
1065
  // buildExecutiveContext (already vector-backed via the consolidator).
1004
1066
  auditionEngine.attachMemorySink( entity => simulation.stateManager.setEntity( entity ) )
1005
1067
 
1068
+ // Every sense lays down a percept trace (SIGNAL_BOUNDARY P0). Wired for all
1069
+ // five, not just the implemented one, because that is the contract a host is
1070
+ // owed: implement a sense and what it senses reaches the faculties that read
1071
+ // percepts, rather than a bus event three subscribers glance at for one tick.
1072
+ //
1073
+ // The tick is a getter, not a value — a sense is ingest-driven and off-tick,
1074
+ // so it has no `react()` to be handed one in, and a captured number would
1075
+ // stamp every percept with whenever assembly ran.
1076
+ //
1077
+ // AuditionEngine opts out (`tracesPercepts = false`); it still gets the wiring
1078
+ // so turning it on is a one-word change when the measurement is done.
1079
+ for( const sense of [ auditionEngine, visionEngine, somatosensationEngine, olfactionEngine, gustationEngine ] )
1080
+ sense.attachPerceptTrace(
1081
+ entity => simulation.stateManager.setEntity( entity ),
1082
+ () => simulation.clock.currentTick,
1083
+ )
1084
+
1085
+ // Referent → address + room. Closes over the state manager because the writer
1086
+ // is deliberately stateless; this is the one seam both send paths cross, so the
1087
+ // translation happens once rather than in each of them.
1088
+ outboxWriter.attachRouting( ( targetEntityId, chosenThread ) => {
1089
+ if( !isReferentId( targetEntityId ) ) return null // already an address
1090
+
1091
+ // Through the O(1) type index rather than the whole entity map: this runs on
1092
+ // every outbound message, and the two types it needs are both indexed.
1093
+ const entities = new Map(
1094
+ [ ...simulation.stateManager.getEntitiesByType( DOSSIER_TYPE ),
1095
+ ...simulation.stateManager.getEntitiesByType( ALIAS_TYPE ) ]
1096
+ .map( e => [ e.id, e ] as const ),
1097
+ )
1098
+
1099
+ // An anchor cannot be delivered to. Find an address the world knows them by —
1100
+ // preferring one on the same platform as the room already chosen, so a reply
1101
+ // in a Discord thread is not addressed to a WhatsApp handle.
1102
+ const aliases = readAliases( entities )
1103
+ const scheme = chosenThread?.split(':')[0]
1104
+ const addresses = [ ...aliases.entries() ]
1105
+ .filter( ( [ , canonical ] ) => canonical === targetEntityId )
1106
+ .map( ( [ alias ] ) => alias )
1107
+ .sort()
1108
+ const address = addresses.find( a => scheme && a.startsWith(`${ scheme }:`) ) ?? addresses[0]
1109
+ if( !address ) return null // nothing known — let the bridge's roster try
1110
+
1111
+ // Only a fallback: a chosen room always wins upstream (see enqueue).
1112
+ const room = defaultHandle( handlesOf( entities, targetEntityId ) )
1113
+ return { targetEntityId: address, ...( room ? { threadId: room.keid } : {} ) }
1114
+ } )
1115
+
1006
1116
  // Salience inputs (§3): weight conversational salience by relationship closeness
1007
1117
  // and active-goal topic overlap. Both are deterministic faculty-state reads.
1008
1118
  auditionEngine.attachAttachmentScore( entityId => attachmentEvaluator.getAttachmentScore( entityId ) )
@@ -1064,6 +1174,10 @@ function _constructCognition(
1064
1174
  affectiveBlender,
1065
1175
  workingMemory,
1066
1176
  episodicConsolidator,
1177
+ // Exposed so shutdown can FLUSH it. The adapter only ever persisted itself from
1178
+ // a 5s debounce timer that no shutdown path awaited, so the index died with the
1179
+ // process — see WillStem.archiveWill.
1180
+ vectorMemory,
1067
1181
  semanticIntegrator,
1068
1182
  spacedRepetition,
1069
1183
  forgettingCurve,
@@ -1211,6 +1325,12 @@ function _registerEngines( simulation: DefaultSimulation, cognition: Cognition,
1211
1325
  activeEngines
1212
1326
  .sort( ( a: any, b: any ) => a.priority - b.priority )
1213
1327
  .forEach( e => simulation.addEngine( e ) )
1328
+
1329
+ // Where this mind ends. Read from the LIVE engine list, not a copy, so an
1330
+ // engine a host registers after assembly is inside the boundary too — its
1331
+ // declared `writes` are machinery, and machinery is not perceived.
1332
+ cognition.exteroception.attachBoundary(
1333
+ () => endogenousTypes( simulation.orchestrator.engines ) )
1214
1334
  }
1215
1335
 
1216
1336
  /**
@@ -1241,25 +1361,26 @@ function _seedIdentity(
1241
1361
  const namePrefix = nameAlreadyInPrompt ? '' : `I am ${config.name}.`
1242
1362
  const fullPersonaText = [ namePrefix, personaText ].filter( Boolean ).join(' ')
1243
1363
 
1244
- const prompt = [
1245
- WILL_CORE_PREAMBLE,
1246
- fullPersonaText ? `\n\n## Who I Am\n${fullPersonaText}` : '',
1247
- profileContext ? `\n\n## My Environment\n${profileContext}` : '',
1248
- ].join('')
1249
-
1250
- simulation.stateManager.setEntity({
1251
- id: 'identity-self',
1252
- type: 'will.identity',
1253
- createdAt: Date.now(),
1254
- updatedAt: Date.now(),
1255
- metadata: {
1256
- name: config.name, // canonical persona name single source of truth
1257
- prompt,
1258
- values: identity.values,
1259
- traits: identity.traits,
1260
- style: identity.style,
1261
- version: 1
1262
- }
1364
+ // Composed for the prompt, but the persona is ALSO stored on its own below —
1365
+ // see cognition/identity.entity. Storing only the composed string is what let
1366
+ // the distiller capture the container's preamble into a tenant's artifact.
1367
+ const prompt = composeIdentityPrompt( fullPersonaText, profileContext )
1368
+
1369
+ // The one place `name` is ever written. Every other writer merges (see
1370
+ // cognition/identity.entity), so from here on the mind's name can only be
1371
+ // changed on purpose — never dropped as a side effect of revising something else.
1372
+ mergeIdentity( simulation.stateManager, {
1373
+ name: config.name, // canonical persona name — single source of truth
1374
+ prompt,
1375
+ // Layer 2 alone — what the artifact will carry. `prompt` is the composed
1376
+ // view for the prompt factory and is recomposed from THIS on every load, so
1377
+ // a woken mind always gets the current build's preamble.
1378
+ persona: fullPersonaText,
1379
+ ...( profileContext ? { environment: profileContext } : {} ),
1380
+ values: identity.values,
1381
+ traits: identity.traits,
1382
+ style: identity.style,
1383
+ version: 1
1263
1384
  })
1264
1385
  }
1265
1386
 
@@ -1291,13 +1412,33 @@ function _seedInitialGoals( simulation: DefaultSimulation, config: WillConfig ):
1291
1412
  */
1292
1413
  function _seedEngineConfigs( simulation: DefaultSimulation, entities: EngineConfigEntity[] ): void {
1293
1414
  for( const cfg of entities )
1294
- simulation.stateManager.setEntity({
1295
- id: cfg.id,
1296
- type: 'engine.config',
1297
- createdAt: Date.now(),
1298
- updatedAt: Date.now(),
1299
- metadata: { engine: cfg.engine, params: cfg.params },
1300
- })
1415
+ mergeEngineConfig( simulation.stateManager, cfg, 'incoming')
1416
+ }
1417
+
1418
+ /**
1419
+ * Fill in engine-config params a restored Will has never seen, WITHOUT touching
1420
+ * the ones it has.
1421
+ *
1422
+ * Seeding runs inside `assembleMind`; the snapshot restore runs after it and
1423
+ * replaces the entity map wholesale, so a Will woke with whatever config it first
1424
+ * hibernated under — for good. Every tunable added after a tenant's first run was
1425
+ * therefore unreachable by that tenant: measured on a live Will, three params
1426
+ * shipped that day (`repeatDamping`, `repeatWindowTicks`, `socialWeight`) were
1427
+ * simply absent from its restored `engine-config-action-selector`, so the code
1428
+ * reading them silently fell back to defaults and the features did nothing.
1429
+ *
1430
+ * A container has to be able to ship a new capability to a tenant already living
1431
+ * in it. Restored values WIN — they carry PMA seeding and whatever the persona has
1432
+ * learned — and only genuinely missing keys are added.
1433
+ */
1434
+ export function backfillEngineConfigs( simulation: DefaultSimulation, entities: EngineConfigEntity[] ): void {
1435
+ for( const cfg of entities ){
1436
+ // 'existing' — state is the authority here. It carries PMA seeding and
1437
+ // whatever the persona has learned, so only genuinely missing keys are added.
1438
+ const added = mergeEngineConfig( simulation.stateManager, cfg, 'existing')
1439
+ if( added.length > 0 )
1440
+ logger.info(`[WillStem] ${cfg.id}: added ${added.length} new param(s) — ${added.join(', ')}`)
1441
+ }
1301
1442
  }
1302
1443
 
1303
1444
  // ── Helpers ──────────────────────────────────────────────────