@mindot/will 0.7.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 (88) hide show
  1. package/README.md +87 -22
  2. package/dist/channels/discord.d.ts +67 -6
  3. package/dist/channels/discord.js +112 -6
  4. package/dist/channels/discord.js.map +1 -1
  5. package/dist/channels/whatsapp.d.ts +1 -1
  6. package/dist/channels/whatsapp.js +4 -1
  7. package/dist/channels/whatsapp.js.map +1 -1
  8. package/dist/cli.js +13733 -11057
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +3729 -1199
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/effectors.d.ts +1 -1
  14. package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
  15. package/package.json +1 -1
  16. package/src/channels/discord.ts +189 -11
  17. package/src/channels/types.ts +90 -0
  18. package/src/channels/whatsapp.ts +13 -4
  19. package/src/cli.ts +9 -4
  20. package/src/cognition/agency/consequence.ts +122 -1
  21. package/src/cognition/agency/conversation.aim.ts +260 -0
  22. package/src/cognition/agency/engines/action.selector.ts +85 -3
  23. package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
  24. package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
  25. package/src/cognition/agency/engines/reafference.engine.ts +129 -2
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +16 -2
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/repertoire.ts +12 -5
  30. package/src/cognition/agency/selection.scoring.ts +33 -0
  31. package/src/cognition/agency/types.ts +35 -0
  32. package/src/cognition/cache/composition.ts +232 -0
  33. package/src/cognition/cache/deliberation.cache.ts +219 -0
  34. package/src/cognition/cache/fingerprint.ts +120 -0
  35. package/src/cognition/cache/types.ts +105 -0
  36. package/src/cognition/config.mirror.entities.ts +109 -1
  37. package/src/cognition/event.schemas.ts +22 -0
  38. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  39. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  40. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  41. package/src/cognition/faculties/executive.engine/context.ts +67 -13
  42. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  43. package/src/cognition/faculties/executive.engine/engine.ts +686 -187
  44. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
  45. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
  46. package/src/cognition/faculties/executive.engine/facet.ts +89 -26
  47. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  48. package/src/cognition/faculties/executive.engine/parser.ts +21 -1
  49. package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
  50. package/src/cognition/faculties/executive.engine/types.ts +69 -0
  51. package/src/cognition/faculties/goal.manager.ts +94 -14
  52. package/src/cognition/faculties/known.entity.tracker.ts +267 -28
  53. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  54. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  55. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  56. package/src/cognition/faculties/self.model.updater.ts +19 -12
  57. package/src/cognition/faculties/social.perception.ts +47 -3
  58. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  59. package/src/cognition/faculties/working.memory.ts +10 -20
  60. package/src/cognition/identity.entity.ts +205 -0
  61. package/src/cognition/index.ts +11 -0
  62. package/src/cognition/memory/vector.adapter.ts +12 -3
  63. package/src/cognition/memory/vector.embedder.ts +54 -7
  64. package/src/cognition/persona.prior.ts +6 -0
  65. package/src/cognition/senses/audition.engine/engine.ts +404 -46
  66. package/src/cognition/senses/base.sense.engine.ts +1 -1
  67. package/src/cognition/senses/index.ts +12 -0
  68. package/src/cognition/social.identity.ts +273 -0
  69. package/src/cognition/utilities/token.tracker.ts +246 -98
  70. package/src/core/orchestrator.ts +38 -0
  71. package/src/host/boot.ts +78 -22
  72. package/src/index.ts +35 -0
  73. package/src/llm/index.ts +415 -97
  74. package/src/llm/routing.ts +204 -0
  75. package/src/llm/summarizer.ts +5 -1
  76. package/src/llm/wire.contracts.ts +19 -0
  77. package/src/pma/index.ts +67 -53
  78. package/src/runners/thin-shim.runner.ts +18 -6
  79. package/src/sdk/will.ts +121 -22
  80. package/src/stem/assembly.audit.ts +1 -0
  81. package/src/stem/guards/identity.coherence.ts +17 -6
  82. package/src/stem/index.ts +82 -5
  83. package/src/stem/mind.ts +327 -79
  84. package/src/stem/policy/arbiter.ts +49 -14
  85. package/src/stem/policy/rule.table.ts +2 -2
  86. package/src/stem/tracts/effector.controller.ts +56 -9
  87. package/src/stem/tracts/outbox.writer.ts +40 -2
  88. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
package/src/sdk/will.ts CHANGED
@@ -29,6 +29,7 @@
29
29
 
30
30
  import { WillStem } from '#stem/index'
31
31
  import type { WillConfig, WillIdentity, Anatomy, InitialGoal, WillModelConfig, WillLLMConfig } from '#stem/mind'
32
+ import { PROVIDER_KEY_ENV, providerKeyFromEnv, type LLMProvider } from '#llm/index'
32
33
  import type { PMASnapshot } from '#pma/index'
33
34
  import type { effectorInvocation } from '#types'
34
35
  import type { EffectorDeclaration, SchemaPrecondition } from '#agency/types'
@@ -55,6 +56,8 @@ export interface Stimulus {
55
56
  speaker?: string
56
57
  /** Conversation/thread id (default = `from`). */
57
58
  thread?: string
59
+ /** True when `thread` is private — just this someone and the Will. See TextMessage.direct. */
60
+ direct?: boolean
58
61
  }
59
62
 
60
63
  /** A message the Will emitted to someone. */
@@ -65,6 +68,20 @@ export interface WillMessage {
65
68
  content: string
66
69
  /** Entity id the Will addressed (the speaker you used in say()/tell(), or a bond). */
67
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
68
85
  }
69
86
 
70
87
  /**
@@ -165,20 +182,42 @@ export interface CreateWillOptions {
165
182
  anatomy?: Anatomy
166
183
  /** Concrete LLM model id, or a per-role map ({ executive, summarizer?,
167
184
  * deliberation?, embedding? } — unset thinking roles fall back to executive).
168
- * Unset → env / provider default. */
185
+ * Unset → env / provider default.
186
+ * @deprecated Pass `llmConfig: { model }` instead — model and transport are
187
+ * one concern. Still honoured; an explicit `llmConfig.model` wins. */
169
188
  model?: string | WillModelConfig
170
- /** Per-Will LLM transport overrides (provider, BYO apiKey, baseUrl, caps).
189
+ /** Per-Will LLM config: provider, model(s), BYO apiKey, baseUrl, caps.
171
190
  * Unset fields fall back to WILL_LLM_* envs. apiKey stays in memory only.
172
- * (Named llmConfig because `llm` is the mock/anthropic MODE switch.) */
191
+ * (Named llmConfig because `llm` is the provider MODE switch.) */
173
192
  llmConfig?: WillLLMConfig
174
193
  /**
175
- * LLM mode. 'mock' (default when no key is present) runs a deterministic
176
- * canned executive — zero keys, zero cost. 'anthropic' calls Claude (needs
177
- * ANTHROPIC_API_KEY / WILL_LLM_* env); 'glm' calls Z.ai's GLM over its
178
- * Anthropic-compatible endpoint (needs ZAI_API_KEY / WILL_LLM_*). Omit to
179
- * auto-detect from whichever key is set.
194
+ * LLM mode which provider the executive speaks to.
195
+ *
196
+ * 'mock' (the default when no key is present) runs a deterministic canned
197
+ * executive: zero keys, zero cost. Every other value names a provider and
198
+ * needs its key, either the provider's own env below or the
199
+ * provider-agnostic WILL_LLM_API_KEY:
200
+ *
201
+ * anthropic ANTHROPIC_API_KEY Claude, native Messages wire
202
+ * glm ZAI_API_KEY Z.ai GLM, Anthropic-compatible wire
203
+ * openai OPENAI_API_KEY OpenAI wire
204
+ * google GOOGLE_API_KEY | GEMINI_API_KEY native Gemini wire
205
+ * deepseek DEEPSEEK_API_KEY OpenAI wire
206
+ * moonshot MOONSHOT_API_KEY Kimi — OpenAI wire
207
+ * qwen DASHSCOPE_API_KEY Alibaba Model Studio — OpenAI wire
208
+ * xai XAI_API_KEY Grok — OpenAI wire
209
+ * minimax MINIMAX_API_KEY OpenAI wire
210
+ * mistral MISTRAL_API_KEY OpenAI wire
211
+ * ollama · vllm local; no key, set `llm` explicitly
212
+ *
213
+ * Any other string works too — it just has to declare its `wire` and
214
+ * `baseUrl` on `llmConfig.providers`. Naming the vendor rather than
215
+ * borrowing `openai` because it speaks that wire is what keeps the
216
+ * completion tape and the cost breakdown honest.
217
+ *
218
+ * Omit to auto-detect from whichever key is set.
180
219
  */
181
- llm?: 'mock' | 'anthropic' | 'glm'
220
+ llm?: 'mock' | LLMProvider
182
221
  /**
183
222
  * Abilities the Will can choose to enact. `name → handler`, or
184
223
  * `name → { handler, description?, cost?, valence?, preconditions? }` to seed
@@ -213,6 +252,41 @@ const AFFECT_EPSILON = 0.02
213
252
 
214
253
  // ── The facade ────────────────────────────────────────────────
215
254
 
255
+
256
+ /**
257
+ * Which provider to talk to when the caller did not say.
258
+ *
259
+ * Checks the provider-agnostic key first (it means "I configured this
260
+ * deliberately"), then each provider's conventional env. The order among
261
+ * providers is detection precedence when several keys happen to be present —
262
+ * it is not a ranking, and no provider here is more supported than another.
263
+ */
264
+ export function detectProvider(): 'mock' | LLMProvider {
265
+ // A provider-agnostic key says nothing about who to send it to. Guessing
266
+ // here is how a key meant for one vendor ends up at another; the guess used
267
+ // to be 'anthropic' unconditionally.
268
+ if( process.env.WILL_LLM_API_KEY ){
269
+ const provider = process.env.WILL_LLM_PROVIDER
270
+ if( !provider )
271
+ throw new Error(
272
+ 'WILL_LLM_API_KEY is set but WILL_LLM_PROVIDER is not — there is no way ' +
273
+ 'to tell which provider that key belongs to. Set WILL_LLM_PROVIDER, or ' +
274
+ 'use a provider-specific key (ANTHROPIC_API_KEY, ZAI_API_KEY, …).'
275
+ )
276
+ return provider as LLMProvider
277
+ }
278
+ // A provider-specific key IS the explicit statement — no guess involved.
279
+ // Order is precedence when several are present, and is append-only: moving an
280
+ // entry silently changes which vendor an existing environment talks to.
281
+ //
282
+ // Read through providerKeyFromEnv rather than raw truthiness, so "is a key
283
+ // set?" and "what is the key?" cannot disagree — a blank `XAI_API_KEY=` would
284
+ // otherwise select xai here and then supply nothing to call it with.
285
+ for( const provider of Object.keys( PROVIDER_KEY_ENV ) )
286
+ if( providerKeyFromEnv( provider ) ) return provider
287
+ return 'mock'
288
+ }
289
+
216
290
  export class Will {
217
291
  /** The underlying WillStem — drop here for the full contract. */
218
292
  readonly stem: WillStem
@@ -261,17 +335,29 @@ export class Will {
261
335
  pma: PMASnapshot,
262
336
  opts: Omit<CreateWillOptions, 'identity'> & { identity?: Partial<WillIdentity> },
263
337
  ): Promise<Will> {
264
- 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 )}`
265
348
  const stem = new WillStem()
266
349
  const will = new Will( stem, id, opts.name )
267
350
 
268
351
  for( const [ name, entry ] of Object.entries( opts.effectors ?? {} ) )
269
352
  will._register( name, entry )
270
353
 
271
- await stem.createWill(
272
- will._buildConfig( id, { ...opts, identity: { prompt: '', ...opts.identity } } ),
273
- true /* startPaused load the artifact before the first tick */,
274
- )
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 */ )
275
361
  stem.loadPMA( id, pma )
276
362
  stem.resumeWill( id )
277
363
  will._attach()
@@ -300,6 +386,8 @@ export class Will {
300
386
  // the Will knows the person as "someone" until a real one is learned. (The live
301
387
  // conversation focus still falls back to the entity id for its Speaker line.)
302
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 } : {} ),
303
391
  } )
304
392
  }
305
393
 
@@ -462,13 +550,22 @@ export class Will {
462
550
 
463
551
  private _buildConfig( id: string, opts: CreateWillOptions ): WillConfig {
464
552
  // Auto-detect from whichever provider key is present; explicit `llm` wins.
465
- const mode = opts.llm
466
- ?? ( process.env.ANTHROPIC_API_KEY ? 'anthropic' : process.env.ZAI_API_KEY ? 'glm' : 'mock')
553
+ // Order is detection precedence when several keys are set, not a
554
+ // recommendation every provider here is a first-class target.
555
+ const mode = opts.llm ?? detectProvider()
467
556
  const useMock = mode === 'mock'
468
- // `llm: 'glm'` selects the provider; an explicit llmConfig still overrides it.
469
- const llmConfig = mode === 'glm'
470
- ? { provider: 'glm' as const, ...opts.llmConfig }
471
- : opts.llmConfig
557
+ // `llm` selects the provider; an explicit llmConfig.provider still wins.
558
+ // Model rides with the transport: `llmConfig.model` is canonical, the
559
+ // top-level `opts.model` is the deprecated spelling of the same thing.
560
+ const llmConfig: WillLLMConfig | undefined = useMock && !opts.llmConfig && opts.model === undefined
561
+ ? undefined
562
+ : {
563
+ ...( mode !== 'mock' ? { provider: mode } : {} ),
564
+ ...opts.llmConfig,
565
+ ...( opts.llmConfig?.model !== undefined ? { model: opts.llmConfig.model }
566
+ : opts.model !== undefined ? { model: opts.model }
567
+ : {} ),
568
+ }
472
569
  return {
473
570
  id, name: opts.name,
474
571
  identity: {
@@ -478,7 +575,6 @@ export class Will {
478
575
  style: opts.identity.style ?? '',
479
576
  },
480
577
  anatomy: opts.anatomy ?? 'mind',
481
- model: opts.model,
482
578
  llm: llmConfig,
483
579
  testMode: useMock,
484
580
  persistentMemory: opts.persist ?? false,
@@ -495,7 +591,10 @@ export class Will {
495
591
  this._unsub = this.stem.addTickListener( this.id, ( _snapshot, _tick, outbox, invocations ) => {
496
592
  // Outbound messages → event + auto delivery-ack.
497
593
  for( const msg of outbox ){
498
- 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
+ } )
499
598
  try { this.stem.confirmMessageDelivery( this.id, msg.id, true ) } catch { /* best-effort */ }
500
599
  }
501
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
+
@@ -18,7 +18,7 @@
18
18
  // ─────────────────────────────────────────────────────────────
19
19
 
20
20
  import type { WillIdentity } from '#stem/mind'
21
- import { LLMDirector, defaultModelFor, type LLMProvider, type LLMCallMeta } from '#llm/index'
21
+ import { LLMDirector, BACKGROUND_DEMAND, type LLMProvider, type LLMCallMeta } from '#llm/index'
22
22
  import type { TokenTracker } from '#cognition/utilities/token.tracker'
23
23
 
24
24
  export interface CoherenceIssue {
@@ -48,7 +48,10 @@ export interface IdentityReviewer {
48
48
  }
49
49
 
50
50
  /** Attribution tag for the one creation-time coherence-review call. */
51
- const COHERENCE_META: LLMCallMeta = { category: 'identity-guard', attribute: 'guard', function: 'identity-coherence' }
51
+ // MODEL_ROUTING W0 a one-shot classification at creation time; constant low
52
+ // demand (structurally background, and it runs before there is a mind whose
53
+ // state could modulate it).
54
+ const COHERENCE_META: LLMCallMeta = { category: 'identity-guard', attribute: 'guard', process: 'cog', function: 'identity-coherence', demand: BACKGROUND_DEMAND }
52
55
 
53
56
  const VALID_KINDS = new Set<CoherenceIssue['kind']>( [ 'contradiction', 'false-capability', 'injection', 'incoherence', 'other' ] )
54
57
 
@@ -112,13 +115,21 @@ export async function reviewIdentityCoherence(
112
115
  input: CoherenceInput,
113
116
  opts: { willId?: string; tokenTracker?: TokenTracker | null } = {},
114
117
  ): Promise<CoherenceResult> {
115
- const provider = ( process.env.WILL_LLM_PROVIDER ?? 'anthropic') as LLMProvider
118
+ // No defaults: an unconfigured environment cannot review a persona, and
119
+ // silently reviewing it with somebody else's model is worse than not running.
120
+ const provider = process.env.WILL_LLM_PROVIDER
121
+ const model = process.env.WILL_LLM_MODEL
122
+ if( !provider || !model )
123
+ return { ok: true, ran: false, issues: [], raw: 'review skipped: WILL_LLM_PROVIDER / WILL_LLM_MODEL not set' }
124
+
116
125
  const director = new LLMDirector({
117
126
  willId: opts.willId ?? 'identity-coherence',
118
- model: process.env.WILL_LLM_MODEL ?? defaultModelFor( provider ),
127
+ model,
119
128
  maxOutputTokens: 512,
120
- apiKey: process.env.WILL_LLM_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? '',
121
- provider,
129
+ // Provider-agnostic key only — the old chain ended at ANTHROPIC_API_KEY,
130
+ // so a Will pointed at another vendor would have sent it an Anthropic key.
131
+ apiKey: process.env.WILL_LLM_API_KEY ?? '',
132
+ provider: provider as LLMProvider,
122
133
  sessionLogger: null,
123
134
  baseUrl: process.env.WILL_LLM_BASE_URL ?? process.env.OPENAI_BASE_URL,
124
135
  // When a per-Will tracker is supplied, the creation-time review records under
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
 
@@ -95,7 +98,7 @@ export interface WillSummary {
95
98
  createdAt: Date
96
99
  lastTickAt: Date | null
97
100
  anatomy: WillConfig['anatomy']
98
- model: WillConfig['model']
101
+ model: NonNullable<WillConfig['llm']>['model']
99
102
  }
100
103
 
101
104
  // Re-export WillConfig so the API layer only imports from manager
@@ -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 ) }
@@ -289,7 +350,7 @@ export class WillStem {
289
350
  willId: config.id,
290
351
  willName: config.name,
291
352
  anatomy: config.anatomy ?? 'mind',
292
- model: config.model ?? null,
353
+ model: config.llm?.model ?? null,
293
354
  startedAt: new Date().toISOString(),
294
355
  })
295
356
 
@@ -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
@@ -942,7 +1019,7 @@ export class WillStem {
942
1019
  createdAt: inst.createdAt,
943
1020
  lastTickAt: inst.lastTickAt,
944
1021
  anatomy: inst.config.anatomy ?? 'mind',
945
- model: inst.config.model,
1022
+ model: inst.config.llm?.model,
946
1023
  }))
947
1024
  }
948
1025