@mindot/will 0.8.0 → 0.9.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 (79) hide show
  1. package/dist/channels/discord.d.ts +67 -6
  2. package/dist/channels/discord.js +112 -6
  3. package/dist/channels/discord.js.map +1 -1
  4. package/dist/channels/whatsapp.d.ts +1 -1
  5. package/dist/channels/whatsapp.js +4 -1
  6. package/dist/channels/whatsapp.js.map +1 -1
  7. package/dist/cli.js +3174 -867
  8. package/dist/cli.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +3236 -1042
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp/effectors.d.ts +1 -1
  13. package/dist/{will-cS6k4uiJ.d.ts → will-DbDj_TEH.d.ts} +752 -17
  14. package/package.json +1 -1
  15. package/src/channels/discord.ts +189 -11
  16. package/src/channels/types.ts +90 -0
  17. package/src/channels/whatsapp.ts +13 -4
  18. package/src/cli.ts +9 -4
  19. package/src/cognition/agency/consequence.ts +122 -1
  20. package/src/cognition/agency/conversation.aim.ts +260 -0
  21. package/src/cognition/agency/engines/action.selector.ts +83 -2
  22. package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
  24. package/src/cognition/agency/engines/reafference.engine.ts +117 -0
  25. package/src/cognition/agency/proactive.communicator.ts +19 -3
  26. package/src/cognition/agency/restart.ts +66 -0
  27. package/src/cognition/agency/selection.scoring.ts +33 -0
  28. package/src/cognition/agency/types.ts +35 -0
  29. package/src/cognition/cache/composition.ts +232 -0
  30. package/src/cognition/cache/deliberation.cache.ts +219 -0
  31. package/src/cognition/cache/fingerprint.ts +120 -0
  32. package/src/cognition/cache/types.ts +105 -0
  33. package/src/cognition/config.mirror.entities.ts +108 -0
  34. package/src/cognition/event.schemas.ts +22 -0
  35. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  36. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  37. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  38. package/src/cognition/faculties/executive.engine/context.ts +67 -13
  39. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  40. package/src/cognition/faculties/executive.engine/engine.ts +552 -131
  41. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
  42. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
  43. package/src/cognition/faculties/executive.engine/facet.ts +81 -26
  44. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  45. package/src/cognition/faculties/executive.engine/parser.ts +21 -1
  46. package/src/cognition/faculties/executive.engine/prompt.factory.ts +167 -19
  47. package/src/cognition/faculties/executive.engine/types.ts +69 -0
  48. package/src/cognition/faculties/goal.manager.ts +94 -14
  49. package/src/cognition/faculties/known.entity.tracker.ts +267 -28
  50. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  51. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  52. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  53. package/src/cognition/faculties/self.model.updater.ts +19 -12
  54. package/src/cognition/faculties/social.perception.ts +47 -3
  55. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  56. package/src/cognition/faculties/working.memory.ts +10 -20
  57. package/src/cognition/identity.entity.ts +205 -0
  58. package/src/cognition/index.ts +7 -0
  59. package/src/cognition/memory/vector.adapter.ts +12 -3
  60. package/src/cognition/memory/vector.embedder.ts +45 -2
  61. package/src/cognition/persona.prior.ts +6 -0
  62. package/src/cognition/senses/audition.engine/engine.ts +404 -46
  63. package/src/cognition/senses/base.sense.engine.ts +1 -1
  64. package/src/cognition/senses/index.ts +12 -0
  65. package/src/cognition/social.identity.ts +273 -0
  66. package/src/cognition/utilities/token.tracker.ts +58 -5
  67. package/src/core/orchestrator.ts +38 -0
  68. package/src/llm/index.ts +25 -8
  69. package/src/llm/routing.ts +6 -0
  70. package/src/llm/summarizer.ts +1 -1
  71. package/src/llm/wire.contracts.ts +19 -0
  72. package/src/pma/index.ts +67 -53
  73. package/src/sdk/will.ts +39 -6
  74. package/src/stem/assembly.audit.ts +1 -0
  75. package/src/stem/guards/identity.coherence.ts +1 -1
  76. package/src/stem/index.ts +79 -2
  77. package/src/stem/mind.ts +172 -55
  78. package/src/stem/tracts/outbox.writer.ts +40 -2
  79. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
package/src/pma/index.ts CHANGED
@@ -32,6 +32,7 @@ import { readFileSync, existsSync } from 'node:fs'
32
32
  import { join } from 'node:path'
33
33
 
34
34
  import type { SimulationState } from '#core/types'
35
+ import { mergeEngineConfig } from '#cognition/config.mirror.entities'
35
36
  import type { DefaultSimulation } from '#core/simulation'
36
37
  import type { Cognition } from '#types'
37
38
  import type {
@@ -45,6 +46,7 @@ import {
45
46
  loadCompetence,
46
47
  type CompetenceSnapshot,
47
48
  } from '#agency/competence.codec'
49
+ import { mergeIdentity, composeIdentityPrompt, readPersona, IDENTITY_ENTITY_ID } from '#cognition/identity.entity'
48
50
 
49
51
  // ── Schema version ─────────────────────────────────────────────
50
52
  // Bump this when any field is removed or semantically changed.
@@ -361,7 +363,12 @@ export class PMADistiller {
361
363
  if( entity.type === 'will.identity'){
362
364
  const m = entity.metadata ?? {}
363
365
  return {
364
- prompt: ( m['prompt'] as string ) ?? '',
366
+ // The TENANT's text only. Capturing the composed `prompt` baked the
367
+ // container's WILL_CORE_PREAMBLE into the artifact, so a woken mind
368
+ // recited whichever build's preamble had distilled it — forever, and
369
+ // invisibly. `readPersona` falls back to stripping it out of a composed
370
+ // prompt, so artifacts written before the split load clean too.
371
+ prompt: readPersona( m ),
365
372
  values: ( m['values'] as string[] ) ?? [],
366
373
  traits: ( m['traits'] as Record<string, number> ) ?? {},
367
374
  traitStats: ( m['traitStats'] as PMAIdentity['traitStats'] ) ?? undefined,
@@ -792,22 +799,31 @@ export class PMALoader {
792
799
  const sm = simulation.stateManager
793
800
 
794
801
  // ── 1. Identity (with enhanced fields) ────────────────────
795
- sm.setEntity({
796
- id: 'identity-self',
797
- type: 'will.identity',
798
- createdAt: Date.now(),
799
- updatedAt: Date.now(),
800
- metadata: {
801
- prompt: pma.identity.prompt,
802
- values: pma.identity.values,
803
- traits: pma.identity.traits,
804
- traitStats: pma.identity.traitStats, // restore the Will's own norm (graded salience B/C)
805
- version: pma.identity.version,
806
- style: pma.identity.style,
807
- socialOrientation: pma.identity.socialOrientation,
808
- trustPropensity: pma.identity.trustPropensity,
809
- memoryPersistence: pma.identity.memoryPersistence,
810
- },
802
+ // MERGING (see cognition/identity.entity). An artifact carries what the mind
803
+ // LEARNED about itself; it does not carry the mind's name, because the name is
804
+ // supplied at boot by whoever is renting the container. This used to replace
805
+ // the entity outright, so `_seedIdentity`'s `name` — written moments earlier
806
+ // in the same wake — was erased before the first tick, every single wake.
807
+ // Recomposed, never restored verbatim: `pma.identity.prompt` is layer 2 (the
808
+ // persona), and layer 1 comes from the build that is running NOW. This is what
809
+ // makes a preamble fix reach minds that already exist.
810
+ const environment = sm.getEntity( IDENTITY_ENTITY_ID )?.metadata?.['environment']
811
+ // Through `readPersona`, NOT raw. An artifact written before the layers were
812
+ // split stores the COMPOSED prompt, so composing over it again gives the mind
813
+ // its own architecture twice. Putting this fallback only in the distiller was
814
+ // not enough — the loader is what every existing artifact goes through.
815
+ const persona = readPersona({ prompt: pma.identity.prompt })
816
+ mergeIdentity( sm, {
817
+ persona,
818
+ prompt: composeIdentityPrompt( persona, typeof environment === 'string' ? environment : undefined ),
819
+ values: pma.identity.values,
820
+ traits: pma.identity.traits,
821
+ traitStats: pma.identity.traitStats, // restore the Will's own norm (graded salience B/C)
822
+ version: pma.identity.version,
823
+ style: pma.identity.style,
824
+ socialOrientation: pma.identity.socialOrientation,
825
+ trustPropensity: pma.identity.trustPropensity,
826
+ memoryPersistence: pma.identity.memoryPersistence,
811
827
  })
812
828
 
813
829
  // ── 1b. Persona (learned self-tuning from the metacognition cycle) ──
@@ -889,55 +905,53 @@ export class PMALoader {
889
905
  const temperamentValence = pma.emotionalBaseline.temperamentValence
890
906
  const reactivity = pma.emotionalBaseline.reactivity
891
907
 
892
- sm.setEntity({
893
- id: 'engine-config-affective-blender',
894
- type: 'engine.config',
895
- createdAt: Date.now(),
896
- updatedAt: Date.now(),
897
- metadata: {
898
- engine: 'affective-blender',
899
- params: {
900
- inertia: 1 - reactivity,
901
- temperamentValence,
902
- },
908
+ // MERGE, never replace — same trap as engine-config-executive. The mirror
909
+ // seeds `emitBlendEvents` here and the PMA carries only `inertia`, so a
910
+ // whole-entity write silently dropped it for every restored Will.
911
+ mergeEngineConfig( sm, {
912
+ id: 'engine-config-affective-blender', engine: 'affective-blender',
913
+ params: {
914
+ inertia: 1 - reactivity,
915
+ temperamentValence,
903
916
  },
904
- })
917
+ }, 'incoming')
905
918
 
906
919
  // ── 6. Behavioral parameters ──────────────────────────────
907
920
  if( pma.behavioral.riskTolerance !== undefined ||
908
921
  pma.behavioral.explorationRate !== undefined ||
909
922
  pma.behavioral.impulsivity !== undefined ){
910
923
 
911
- sm.setEntity({
912
- id: 'engine-config-executive',
913
- type: 'engine.config',
914
- createdAt: Date.now(),
915
- updatedAt: Date.now(),
916
- metadata: {
917
- engine: 'executive',
918
- params: {
919
- riskTolerance: pma.behavioral.riskTolerance ?? 0.5,
920
- explorationRate: pma.behavioral.explorationRate ?? 0.3,
921
- impulsivity: pma.behavioral.impulsivity ?? 0.3,
922
- },
924
+ // MERGE onto the seeded mirror, never replace it.
925
+ //
926
+ // createWill() seeds engine-config-executive first (executiveInterval,
927
+ // cooldownTicks, deliberateThreshold, maxFacets); loadPMA() runs after. A
928
+ // whole-entity write here dropped every param the PMA does not carry — so
929
+ // for every Will restored from an artifact (i.e. every production Will), the
930
+ // executive's dual-process gate lost its base, `readBaseParams` returned
931
+ // nothing for `deliberateThreshold`, and consolidatePrior skipped its
932
+ // adjustments outright: the analytical/decisiveness edges that are supposed
933
+ // to develop how readily this mind stops to think had no base to move.
934
+ mergeEngineConfig( sm, {
935
+ id: 'engine-config-executive', engine: 'executive',
936
+ params: {
937
+ riskTolerance: pma.behavioral.riskTolerance ?? 0.5,
938
+ explorationRate: pma.behavioral.explorationRate ?? 0.3,
939
+ impulsivity: pma.behavioral.impulsivity ?? 0.3,
923
940
  },
924
- })
941
+ }, 'incoming')
925
942
  }
926
943
 
927
944
  // Configure memory persistence for ForgettingCurve
928
945
  if( pma.identity.memoryPersistence !== undefined ){
929
- sm.setEntity({
930
- id: 'engine-config-forgetting',
931
- type: 'engine.config',
932
- createdAt: Date.now(),
933
- updatedAt: Date.now(),
934
- metadata: {
935
- engine: 'forgetting-curve',
936
- params: {
937
- baseForgettingRate: 1 - ( pma.identity.memoryPersistence * 0.7 ),
938
- },
946
+ // MERGE — the mirror seeds emotionProtection / pruningThreshold /
947
+ // maxPrunePerTick here and the PMA carries only baseForgettingRate, so a
948
+ // whole-entity write dropped three params on every restore.
949
+ mergeEngineConfig( sm, {
950
+ id: 'engine-config-forgetting', engine: 'forgetting-curve',
951
+ params: {
952
+ baseForgettingRate: 1 - ( pma.identity.memoryPersistence * 0.7 ),
939
953
  },
940
- })
954
+ }, 'incoming')
941
955
  }
942
956
 
943
957
  // ── 7. Relationship stubs ─────────────────────────────────
package/src/sdk/will.ts CHANGED
@@ -56,6 +56,8 @@ export interface Stimulus {
56
56
  speaker?: string
57
57
  /** Conversation/thread id (default = `from`). */
58
58
  thread?: string
59
+ /** True when `thread` is private — just this someone and the Will. See TextMessage.direct. */
60
+ direct?: boolean
59
61
  }
60
62
 
61
63
  /** A message the Will emitted to someone. */
@@ -66,6 +68,20 @@ export interface WillMessage {
66
68
  content: string
67
69
  /** Entity id the Will addressed (the speaker you used in say()/tell(), or a bond). */
68
70
  to: string
71
+ /**
72
+ * The conversation this belongs to — the `thread` from the `perceive()` that
73
+ * prompted it. Absent when the Will spoke unprompted, which genuinely has no
74
+ * thread.
75
+ *
76
+ * WHERE, not just to whom. The engine knew this the whole way down —
77
+ * `OutboxMessage.threadId` carries it — and the projection dropped it here, so
78
+ * a channel adapter had nothing to answer INTO and had to guess from a roster.
79
+ * Observed live: a DM arrived on `discord:1532693…`, she answered it correctly
80
+ * and in seconds, and the reply went to the shared server channel because that
81
+ * was the last room the roster had seen this person in. From the operator's
82
+ * side she had simply ignored him.
83
+ */
84
+ thread?: string
69
85
  }
70
86
 
71
87
  /**
@@ -319,17 +335,29 @@ export class Will {
319
335
  pma: PMASnapshot,
320
336
  opts: Omit<CreateWillOptions, 'identity'> & { identity?: Partial<WillIdentity> },
321
337
  ): Promise<Will> {
322
- const id = opts.id ?? `${slug( opts.name )}-${Math.random().toString( 36 ).slice( 2, 8 )}`
338
+ // Waking IS being the same mind, so the id continues from the artifact unless the
339
+ // caller overrides it. The id is the path key for everything durable that lives
340
+ // OUTSIDE the artifact — `data/wills/<id>/vector_index`, snapshots, session logs.
341
+ // Minting a fresh `name-<random>` here gave a woken mind a brand-new, empty vector
342
+ // store every boot: identity/beliefs/goals returned (those are in the artifact) while
343
+ // episodic recall came back permanently empty, and the orphaned index was left behind
344
+ // on disk. Observed: four boots, four `lora-*` directories, four 4KB indexes, and a
345
+ // mind that concluded at 100% confidence that its own channel might not be viable
346
+ // because it could not recall a conversation it had just had.
347
+ const id = opts.id ?? pma.willId ?? `${slug( opts.name )}-${Math.random().toString( 36 ).slice( 2, 8 )}`
323
348
  const stem = new WillStem()
324
349
  const will = new Will( stem, id, opts.name )
325
350
 
326
351
  for( const [ name, entry ] of Object.entries( opts.effectors ?? {} ) )
327
352
  will._register( name, entry )
328
353
 
329
- await stem.createWill(
330
- will._buildConfig( id, { ...opts, identity: { prompt: '', ...opts.identity } } ),
331
- true /* startPaused load the artifact before the first tick */,
332
- )
354
+ const config = will._buildConfig( id, { ...opts, identity: { prompt: '', ...opts.identity } } )
355
+ // Say out loud that this identity is a placeholder. Otherwise the creation
356
+ // guard warns about the empty persona we just constructed on purpose — three
357
+ // alarms per wake, drowning the real check that runs when the artifact loads.
358
+ config.identityFromArtifact = true
359
+
360
+ await stem.createWill( config, true /* startPaused — load the artifact before the first tick */ )
333
361
  stem.loadPMA( id, pma )
334
362
  stem.resumeWill( id )
335
363
  will._attach()
@@ -358,6 +386,8 @@ export class Will {
358
386
  // the Will knows the person as "someone" until a real one is learned. (The live
359
387
  // conversation focus still falls back to the entity id for its Speaker line.)
360
388
  ...( stimulus.speaker ? { speakerName: stimulus.speaker } : {} ),
389
+ // Omitted rather than defaulted: an unknown room is not known to be public.
390
+ ...( stimulus.direct !== undefined ? { direct: stimulus.direct } : {} ),
361
391
  } )
362
392
  }
363
393
 
@@ -561,7 +591,10 @@ export class Will {
561
591
  this._unsub = this.stem.addTickListener( this.id, ( _snapshot, _tick, outbox, invocations ) => {
562
592
  // Outbound messages → event + auto delivery-ack.
563
593
  for( const msg of outbox ){
564
- this._emitMessage( { id: msg.id, content: msg.content, to: msg.targetEntityId } )
594
+ this._emitMessage( {
595
+ id: msg.id, content: msg.content, to: msg.targetEntityId,
596
+ ...( msg.threadId ? { thread: msg.threadId } : {} ),
597
+ } )
565
598
  try { this.stem.confirmMessageDelivery( this.id, msg.id, true ) } catch { /* best-effort */ }
566
599
  }
567
600
  // Effector invocations → project the motor act, then run the handler → ack.
@@ -86,3 +86,4 @@ export function auditAssemblyWiring( engines: readonly SimulationEngine[] ): Wir
86
86
  export function wiringKeys( records: WiringRecord[], status: WiringRecord['status'] ): string[] {
87
87
  return records.filter( r => r.status === status ).map( r => `${r.engine}.${r.method}`).sort()
88
88
  }
89
+
@@ -51,7 +51,7 @@ export interface IdentityReviewer {
51
51
  // MODEL_ROUTING W0 — a one-shot classification at creation time; constant low
52
52
  // demand (structurally background, and it runs before there is a mind whose
53
53
  // state could modulate it).
54
- const COHERENCE_META: LLMCallMeta = { category: 'identity-guard', attribute: 'guard', function: 'identity-coherence', demand: BACKGROUND_DEMAND }
54
+ const COHERENCE_META: LLMCallMeta = { category: 'identity-guard', attribute: 'guard', process: 'cog', function: 'identity-coherence', demand: BACKGROUND_DEMAND }
55
55
 
56
56
  const VALID_KINDS = new Set<CoherenceIssue['kind']>( [ 'contradiction', 'false-capability', 'injection', 'incoherence', 'other' ] )
57
57
 
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'
@@ -39,10 +39,13 @@ 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
41
  import { externalSchemas } from '#agency/schemas/external'
42
+ import { inFlightOnRestore } from '#agency/restart'
43
+ import { buildEngineConfigEntities } from '#cognition/config.mirror.entities'
42
44
  import type { EffectorDeclaration } from '#agency/types'
43
45
  import { SensoryController } from '#stem/tracts/sensory.controller'
44
46
  import { BiographyWriter } from '#stem/tracts/biography.writer'
45
47
  import { HealthReporter } from '#stem/tracts/health.reporter'
48
+ import { mergeIdentity } from '#cognition/identity.entity'
46
49
 
47
50
  // ── Types ─────────────────────────────────────────────────────
48
51
 
@@ -224,8 +227,66 @@ export class WillStem {
224
227
  try {
225
228
  const previousState = await simulation.snapshotManager.loadLatestFromStorage()
226
229
  if( previousState ){
230
+ // Work that was in flight when the mind slept does not resume — see
231
+ // agency/restart.ts. Dropped BEFORE restore so it never enters live
232
+ // state at all, rather than being swept on some later tick.
233
+ const inFlight = inFlightOnRestore( previousState.entities )
234
+ for( const id of inFlight ) previousState.entities.delete( id )
235
+
227
236
  simulation.stateManager.restore( previousState, { entities: true, metrics: false } )
228
- logger.info(`[WillStem] Restored snapshot for ${config.id} — ${previousState.entities.size} entities loaded`)
237
+
238
+ // TIME MUST NOT GO BACKWARDS.
239
+ //
240
+ // Entities come back stamped with the tick they were written at, and
241
+ // the orchestrator overwrites the StateManager's tick from the CLOCK
242
+ // every tick (`_clock.tick()` → `updateClock`) — so restoring the
243
+ // manager's tick alone is undone immediately. The clock is the source
244
+ // of truth and has to resume too.
245
+ //
246
+ // Without this, every `tick - stampedTick` in the codebase computed a
247
+ // negative age. Measured: an awaiting intent read `-589 ticks`, so it
248
+ // could never time out, and the selector's staleness decay inverted
249
+ // into amplification (`1 - (-39 × 0.5)` = 20.6×) — a 0.47 incumbent
250
+ // scoring 9.74, unpreemptable, holding the channel against every other
251
+ // contact indefinitely and across restarts. It also means tick-stamped
252
+ // entity ids (`affordance-${tick}-…`, `agency-outcome-${tick}-…`) stop
253
+ // colliding with a previous session's.
254
+ simulation.clock.setTick( previousState.tick )
255
+
256
+ // The restore above replaced the entity map wholesale, including the
257
+ // engine-config mirror `assembleMind` had just seeded — so a Will woke
258
+ // with whatever config it FIRST hibernated under, and every tunable
259
+ // added since was unreachable to it. Restored values win; only params it
260
+ // has never seen are added.
261
+ backfillEngineConfigs( simulation, buildEngineConfigEntities( config, resolveExecutiveInterval( config ) ) )
262
+
263
+ // The same wholesale replacement takes `identity-self` — so the name
264
+ // `_seedIdentity` wrote moments ago is replaced by whatever the snapshot
265
+ // carries, which for any mind hibernated before the merging writer
266
+ // existed is NO NAME AT ALL.
267
+ //
268
+ // Caught only by booting: making every writer merge is necessary and was
269
+ // not sufficient, because restore is not a writer — it is the whole map
270
+ // arriving at once, and it lands between `_seedIdentity` and `loadPMA`.
271
+ // A live Will woke from a repaired build and still told her operator "my
272
+ // self-model says I'm Will".
273
+ //
274
+ // ONLY the name is re-asserted. Everything else on this entity — prompt,
275
+ // values, traits, traitStats, style — is the mind's own accumulated
276
+ // self-knowledge and must come from the snapshot, not from boot config.
277
+ // The name is the one field the container supplies and the tenant never
278
+ // learns, which is exactly why it is the one field a restore may not eat.
279
+ if( config.name ){
280
+ const restored = mergeIdentity( simulation.stateManager, { name: config.name } )
281
+ if( restored.length )
282
+ logger.info(`[WillStem] identity-self: re-asserted name '${config.name}' after restore`)
283
+ }
284
+
285
+ logger.info(
286
+ `[WillStem] Restored snapshot for ${config.id} — ${previousState.entities.size} entities loaded, ` +
287
+ `resuming at tick ${previousState.tick}` +
288
+ ( inFlight.length ? ` (dropped ${inFlight.length} in-flight)` : '')
289
+ )
229
290
  }
230
291
  }
231
292
  catch( err ){ logger.warn(`[WillStem] Snapshot restore failed for ${config.id} — starting fresh:`, err ) }
@@ -459,6 +520,12 @@ export class WillStem {
459
520
  instance.simulation.snapshotManager.persistNow( pauseState )
460
521
  .catch( err => logger.error(`[WillStem] snapshot persist failed on pause (${id}):`, err ))
461
522
 
523
+ // The vector index lives outside the snapshot and only ever persisted itself from
524
+ // a debounce timer nothing awaited — so it died with the process, and every
525
+ // restart cold-started with an EMPTY index no matter how much was consolidated.
526
+ instance.cognition.vectorMemory?.persist()
527
+ .catch( ( err: unknown ) => logger.error(`[WillStem] vector index persist failed on pause (${id}):`, err ))
528
+
462
529
  this._biography.writeSessionSummary( instance )
463
530
  this._biography.writeEmotionalBiographySummary( instance )
464
531
  instance.sessionLogger?.close()
@@ -574,6 +641,16 @@ export class WillStem {
574
641
  instance.simulation.stateManager.applyCommands( flushCmds )
575
642
  const archiveState = instance.simulation.stateManager.snapshot()
576
643
  await instance.simulation.snapshotManager.persistNow( archiveState )
644
+
645
+ // …and the vector index, which is NOT part of that snapshot. Awaited here (unlike
646
+ // the pause path) because the process usually exits straight after this: a
647
+ // fire-and-forget write would simply be lost, which is exactly how a mind ended a
648
+ // session having consolidated episodes and left a 198-byte index on disk.
649
+ // Indexing runs in the background (it must not stall the tick loop), so drain it
650
+ // first — otherwise the last episodes consolidated are still mid-embed when the
651
+ // index is written and are absent from the file this session leaves behind.
652
+ await instance.cognition.episodicConsolidator.flushIndexing()
653
+ await instance.cognition.vectorMemory?.persist()
577
654
  }
578
655
 
579
656
  // Session-biography writers (behavioral + emotional) extracted to