@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
@@ -24,7 +24,8 @@ import type {
24
24
  SimulationContext,
25
25
  ReadonlySimulationState,
26
26
  ReasoningFootprint,
27
- StateCommands
27
+ StateCommands,
28
+ EntityInput
28
29
  } from '#core/types'
29
30
  import { AsyncEngine } from '#core/async.engine'
30
31
  import type { IntermediateStream } from '#core/async.engine'
@@ -42,6 +43,9 @@ import { GenerativeModel } from '#cognition/generative.model'
42
43
  import { ExecutiveSummarizer } from '#llm/summarizer'
43
44
  import { ExecutiveFacet, type ExecutiveFacetHandle } from '#faculties/executive.engine/facet'
44
45
  import type { ExecutiveOutputFull, IdeationCandidate } from '#faculties/executive.engine/types'
46
+ import { DeliberationCache } from '#cognition/cache/deliberation.cache'
47
+ import { extractFingerprint } from '#cognition/cache/fingerprint'
48
+ import type { DeliberationCacheConfig, DeliberationCacheSnapshot } from '#cognition/cache/types'
45
49
  import {
46
50
  PromptFactory,
47
51
  type PromptDependencies,
@@ -60,19 +64,21 @@ import {
60
64
  type GatingState
61
65
  } from '#faculties/executive.engine/gating'
62
66
  import { LLMDirector } from '#llm/index'
63
- import { defaultModelFor, type LLMProvider } from '#llm/index'
67
+ import { MOCK_PROVIDER, MOCK_MODEL } from '#llm/index'
68
+ import { getCompletionSource } from '#core/completion.recorder'
69
+ import { providerKeyFromEnv, type LLMProvider, type LLMCallMeta, type ProviderCredential, type LLMWire } from '#llm/index'
70
+ import type { ModelRouter } from '#llm/routing'
64
71
  import { buildFallbackOutput, parseResponse } from '#faculties/executive.engine/parser'
65
72
  import { selectProcess, ideationTemperature, DELIBERATE_THRESHOLD } from '#faculties/executive.engine/effort.gate'
66
73
  import { proposeCandidates } from '#faculties/executive.engine/deliberate.reasoning'
67
74
  import { readEffectiveParams } from '#cognition/persona.prior'
68
- import { MessageQueue } from '#faculties/executive.engine/messages'
69
75
  import {
70
76
  buildStateCommands,
71
77
  publishCognitiveEvents,
72
78
  type CommandDependencies
73
79
  } from '#faculties/executive.engine/commands'
74
80
  import { DeferredEffectQueue } from '#faculties/executive.engine/deferred.effects'
75
- import { EscalationBuffer } from '#faculties/executive.engine/escalation.buffer'
81
+ import { EscalationBuffer, type HandoffBody } from '#faculties/executive.engine/escalation.buffer'
76
82
  import { FacetSupervisor } from '#faculties/executive.engine/facet.supervisor'
77
83
  import type { EngineResult } from '#core/orchestrator'
78
84
  import type { Duration } from '#core/types'
@@ -81,6 +87,17 @@ import { wallClock } from '#core/wall.clock'
81
87
  // Re-export for compatibility
82
88
  export { ExecutiveFacet, type ExecutiveFacetHandle }
83
89
 
90
+ /**
91
+ * How hard a reasoning facet pulls on attention, as an `attention.demand` urgency.
92
+ *
93
+ * Deliberately below 1: holding a live conversation is a real claim on attention,
94
+ * but the allocator sorts candidates into `maxFoci` slots by salience, and a facet
95
+ * that always outranked everything would starve perception — the mind would stop
96
+ * noticing the world whenever it was talking. At 0.7 an urgent percept still wins
97
+ * a slot; routine traffic does not.
98
+ */
99
+ const FACET_ATTENTION_URGENCY = 0.7
100
+
84
101
  // ── Engine config ───────────────────────────────────────────
85
102
 
86
103
  export interface ExecutiveEngineConfig {
@@ -128,7 +145,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
128
145
  private _testMode = false
129
146
 
130
147
  // ── Message queue ──────────────────────────────────────────
131
- private _messageQueue = new MessageQueue()
132
148
 
133
149
  // ── Action diversity tracking ──────────────────────────────
134
150
  private _recentActionTypes: string[] = []
@@ -143,16 +159,25 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
143
159
  private _lastExecutiveOutput: ExecutiveOutputFull | null = null
144
160
  private _lastExecutiveTick: number = -100
145
161
 
162
+ // ── DeliberationCache (optional fast path) ─────────────────
163
+ private _cache: DeliberationCache | null = null
164
+ private _cacheRestored = false
165
+ private _pendingVerify: { fingerprint: Float32Array; cachedActionTypes: string[] } | null = null
166
+ // Last cache outcome this cycle, surfaced as metrics/events from the committed path.
167
+ private _lastCacheHit = false
168
+ private _lastCacheConfidence = 0
169
+ private _lastCacheNeighborCount = 0
170
+
146
171
  // ── Injected dependencies ──────────────────────────────────
147
172
  private _willId: string | null = null
148
- /** Per-Will, per-role model ids (config.model, resolved in mind.ts). */
149
- private _models: { executive: string | null; summarizer: string | null; deliberation: string | null; conversation: string | null } =
150
- { executive: null, summarizer: null, deliberation: null, conversation: null }
173
+ /**
174
+ * The Will's default model (config.model's `executive` role, resolved in
175
+ * mind.ts). Every other role reaches its model through the router — see
176
+ * `compileRoleRouter`.
177
+ */
178
+ private _modelId: string | null = null
151
179
  /** Per-Will LLM transport overrides (config.llm) — env fallbacks apply per field. */
152
- private _llm: { provider?: string; apiKey?: string; baseUrl?: string; maxOutputTokens?: number; timeoutMs?: number } | null = null
153
- /** One director per distinct model — same config, different model. Shared
154
- * tracker/recorder/willId, so ledger attribution and replay hold per role. */
155
- private _directorCache = new Map<string, LLMDirector>()
180
+ private _llm: { provider?: string; apiKey?: string; baseUrl?: string; maxOutputTokens?: number; timeoutMs?: number; credentials?: Partial<Record<string, ProviderCredential>>; router?: ModelRouter | null; wire?: LLMWire } | null = null
156
181
  private _workingMemory: WorkingMemory | null = null
157
182
  private _goalManager: GoalManager | null = null
158
183
  private _episodicConsolidator: EpisodicConsolidator | null = null
@@ -172,7 +197,15 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
172
197
  // engine keeps only the bus-subscription guard, since those subscriptions
173
198
  // feed the gating salience buffer and are shared with the escalation path.
174
199
  private readonly _facetSupervisor = new FacetSupervisor()
175
- private _facetSyncSubscribed = false
200
+
201
+ /**
202
+ * Who each live facet is engaged with, learned from `executive.facet.sync`.
203
+ * Keyed by facetId; the last sync wins. Rendered into the master's own prompt so
204
+ * the singular seat can reason across its conversations "as if they were sitting
205
+ * at the same table" — which it cannot do while it only knows facet numbers.
206
+ * Stale entries age out on read (see _activeConversations).
207
+ */
208
+ private _facetSubjects = new Map<string, { entityId: string; name?: string; tick: number }>()
176
209
 
177
210
  // ── Cognitive models ───────────────────────────────────────
178
211
  private readonly _model = new GenerativeModel()
@@ -184,6 +217,17 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
184
217
  // ── Last state reference (for onReasoningComplete and facets) ─
185
218
  private _lastStateRef: ReadonlySimulationState | null = null
186
219
 
220
+ /**
221
+ * The tick currently being processed, refreshed every react() — distinct from
222
+ * `_lastStateRef` (which tracks the REASONING tick and must not move under
223
+ * onReasoningComplete) and from `_lastExecutiveTick` (the last cycle that ran).
224
+ *
225
+ * Off-tick arrivals — a facet handoff, in particular — need to be stamped with
226
+ * when they actually happened. Using `_lastExecutiveTick` for that dated them to
227
+ * the last master cycle, which can be hundreds of ticks behind.
228
+ */
229
+ private _currentTick = 0
230
+
187
231
  // ── Deferred manager side-effects (FN11) ───────────────────
188
232
  // Commit-gated queue for the mirroring manager writes returned by
189
233
  // buildStateCommands. The queue runs them only after the orchestrator
@@ -228,8 +272,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
228
272
  goallessTickCount: 0,
229
273
  lowValenceTickCount: 0
230
274
  }
231
-
232
- this._ensureFacetSyncSubscription()
233
275
  }
234
276
 
235
277
  // ── Dependency injection ───────────────────────────────────
@@ -253,7 +295,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
253
295
  /** Called by CognitiveOrchestrator.addEngine() — injects the shared bus. */
254
296
  attachBus( bus: CognitiveBus ): void {
255
297
  this._bus = bus
256
- this._ensureFacetSyncSubscription()
257
298
  }
258
299
 
259
300
  /**
@@ -267,13 +308,18 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
267
308
 
268
309
  set willId( willId: string ){ this._willId = willId }
269
310
 
270
- /** Per-Will role models (config.model, resolved). Set before the first tick. */
271
- set models( m: { executive: string | null; summarizer: string | null; deliberation: string | null; conversation: string | null } ){ this._models = m }
272
- get models(): { executive: string | null; summarizer: string | null; deliberation: string | null; conversation: string | null } { return this._models }
311
+ /**
312
+ * The Will's default model. Set before the first tick.
313
+ *
314
+ * This replaced a four-role map (W7): the other roles are routing rules now,
315
+ * compiled in mind.ts, so the engine holds one model and one router rather
316
+ * than a model per role plus a router.
317
+ */
318
+ set modelId( id: string | null ){ this._modelId = id }
319
+ get modelId(): string | null { return this._modelId }
320
+
273
321
  /** Per-Will LLM transport overrides (config.llm). Set before the first tick. */
274
- set llm( c: { provider?: string; apiKey?: string; baseUrl?: string; maxOutputTokens?: number; timeoutMs?: number } | null ){ this._llm = c }
275
- /** The executive-role model id (back-compat read). */
276
- get modelId(): string | null { return this._models.executive }
322
+ set llm( c: { provider?: string; apiKey?: string; baseUrl?: string; maxOutputTokens?: number; timeoutMs?: number; credentials?: Partial<Record<string, ProviderCredential>>; router?: ModelRouter | null; wire?: LLMWire } | null ){ this._llm = c }
277
323
 
278
324
  // ── Public surface ─────────────────────────────────────────
279
325
 
@@ -301,48 +347,108 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
301
347
  * and subscribe() to receive facet decisions.
302
348
  */
303
349
  /** Get-or-create the director for a model id (shared config, per-Will). */
304
- private _directorFor( model: string ): LLMDirector {
305
- let d = this._directorCache.get( model )
306
- if( !d ){
307
- // Per-Will transport overrides first (BYO keys), env per field otherwise.
308
- d = new LLMDirector( {
309
- willId: this._willId!,
310
- model,
311
- maxOutputTokens: this._llm?.maxOutputTokens ?? parseInt( process.env.WILL_MAX_OUTPUT_TOKENS ?? '8096'),
312
- // Provider-agnostic key; falls back to ANTHROPIC_API_KEY for back-compat.
313
- apiKey: this._llm?.apiKey ?? process.env.WILL_LLM_API_KEY ?? process.env.ANTHROPIC_API_KEY ?? '',
314
- provider: ( this._llm?.provider ?? process.env.WILL_LLM_PROVIDER ?? 'anthropic') as LLMProvider,
315
- // Optional base-URL override (e.g. Ollama / Azure / self-hosted). Unset →
316
- // the director uses the provider's official endpoint.
317
- baseUrl: this._llm?.baseUrl ?? process.env.WILL_LLM_BASE_URL ?? process.env.OPENAI_BASE_URL,
318
- timeoutMs: this._llm?.timeoutMs ?? ( process.env.WILL_LLM_TIMEOUT_MS ? parseInt( process.env.WILL_LLM_TIMEOUT_MS ) : undefined ),
319
- sessionLogger: this._sessionLogger,
320
- mock: this._testMode,
321
- // Inject the per-Will tracker (R4) so live calls record usage here, not
322
- // through a process global. null is fine — the director skips recording.
323
- tokenTracker: this._tokenTracker,
324
- } )
325
- this._directorCache.set( model, d )
326
- }
327
- return d
350
+ /**
351
+ * The provider, from config or environment — never guessed.
352
+ *
353
+ * This used to default to 'anthropic', which is how a Will configured for one
354
+ * vendor could quietly talk to another. An unset provider is a configuration
355
+ * error, and saying so at construction is far cheaper than a 401 mid-tick.
356
+ */
357
+ private _requireProvider(): LLMProvider {
358
+ const provider = this._llm?.provider ?? process.env.WILL_LLM_PROVIDER
359
+ ?? ( this._noLiveCalls() ? MOCK_PROVIDER : undefined )
360
+ if( !provider )
361
+ throw new Error(
362
+ 'No LLM provider configured. Set one on the Will (llm.provider) or in ' +
363
+ 'the environment (WILL_LLM_PROVIDER) the engine carries no default.'
364
+ )
365
+ return provider as LLMProvider
366
+ }
367
+
368
+ /**
369
+ * True when this Will cannot make a live call, so provider/model are not
370
+ * required: mock mode, or a replay re-feeding recorded completions.
371
+ */
372
+ private _noLiveCalls(): boolean {
373
+ return this._testMode || ( !!this._willId && !!getCompletionSource( this._willId ) )
374
+ }
375
+
376
+ /**
377
+ * Build this Will's one and only director.
378
+ *
379
+ * There used to be a cache of them, keyed by model, because the per-role
380
+ * model map had no other way to make a role use a different model. Routing
381
+ * gave it one — the role map now compiles to rules (see `compileRoleRouter`)
382
+ * and a single director resolves every call's endpoint per call. That is also
383
+ * strictly more faithful: a facet follows the work it is doing rather than
384
+ * whatever role it happened to be spawned under.
385
+ */
386
+ private _buildDirector( model: string ): LLMDirector {
387
+ // Resolved first: the key fallback below is keyed by it.
388
+ const provider = this._requireProvider()
389
+
390
+ // Per-Will transport overrides first (BYO keys), env per field otherwise.
391
+ return new LLMDirector( {
392
+ willId: this._willId!,
393
+ model,
394
+ maxOutputTokens: this._llm?.maxOutputTokens ?? parseInt( process.env.WILL_MAX_OUTPUT_TOKENS ?? '8096'),
395
+ // Config, then the provider-agnostic env, then THIS provider's own env.
396
+ // The last step is not the fallback W9 removed: that one ended at
397
+ // ANTHROPIC_API_KEY for every provider, so a Will pointed elsewhere sent
398
+ // Anthropic's key to a stranger. This one can only ever read the key
399
+ // belonging to the provider actually configured.
400
+ apiKey: this._llm?.apiKey ?? process.env.WILL_LLM_API_KEY ?? providerKeyFromEnv( provider ) ?? '',
401
+ provider,
402
+ // Optional base-URL override (e.g. Ollama / Azure / self-hosted). Unset →
403
+ // the director uses the provider's official endpoint.
404
+ baseUrl: this._llm?.baseUrl ?? process.env.WILL_LLM_BASE_URL ?? process.env.OPENAI_BASE_URL,
405
+ timeoutMs: this._llm?.timeoutMs ?? ( process.env.WILL_LLM_TIMEOUT_MS ? parseInt( process.env.WILL_LLM_TIMEOUT_MS ) : undefined ),
406
+ sessionLogger: this._sessionLogger,
407
+ mock: this._testMode,
408
+ // Inject the per-Will tracker (R4) so live calls record usage here, not
409
+ // through a process global. null is fine — the director skips recording.
410
+ tokenTracker: this._tokenTracker,
411
+ // Credentials for routed calls, narrowed by the stem from the host's
412
+ // per-provider map. Prices from that same map ride to the TokenTracker
413
+ // instead, so nothing carries pricing into the call path.
414
+ ...( this._llm?.credentials ? { credentials: this._llm.credentials } : {} ),
415
+ // Per-call model selection — the host's router chained with the rules
416
+ // compiled from the per-role model map.
417
+ ...( this._llm?.router ? { router: this._llm.router } : {} ),
418
+ // Dialect for the default provider — required for anything outside the
419
+ // known set, so the engine never guesses how to talk to an endpoint.
420
+ ...( this._llm?.wire ? { wire: this._llm.wire } : {} ),
421
+ } )
328
422
  }
329
423
 
330
- spawnFacet( role?: 'deliberation' | 'conversation' | 'outreach' | 'supervision'): { attention: 'available' | 'full', handle?: ExecutiveFacetHandle } {
424
+ /**
425
+ * Spawn a facet.
426
+ *
427
+ * `role` declares the facet's intent at the call site. It no longer selects a
428
+ * model: that used to happen here, pinning a facet for life to whatever role
429
+ * it was spawned under, and it now happens per call from the focus function
430
+ * the caller sets immediately afterwards (W7). The two always agreed — every
431
+ * spawn site sets a focus whose `function` matches its role — so the routed
432
+ * answer is the same one, decided later and from the work itself.
433
+ */
434
+ spawnFacet(
435
+ role?: 'deliberation' | 'conversation' | 'outreach' | 'supervision',
436
+ /**
437
+ * What this facet is FOR — see `FacetSpawnDeps.key`. Two spawns with the same
438
+ * key get the same facet, so callers no longer each invent their own dedup
439
+ * (and `authorOutreach`, which had none, no longer opens a rival facet on a
440
+ * person the mind is already talking to).
441
+ */
442
+ key?: string,
443
+ ): { attention: 'available' | 'full', handle?: ExecutiveFacetHandle } {
444
+ void role
331
445
  // Delegate to FacetSupervisor (R5-g-3), passing the current engine
332
446
  // attachments. The supervisor owns the registry + attention budget and
333
447
  // performs the throw-checks on bus / director / state ref.
334
- // A role with its own configured model gets that role's director; every
335
- // other facet shares the executive's (one self, role-appropriate depth).
336
- // Outreach speaks with the conversation voice; supervision thinks with the
337
- // executive's depth.
338
- const roleModel =
339
- role === 'deliberation' ? this._models.deliberation :
340
- role === 'conversation' || role === 'outreach' ? this._models.conversation :
341
- null
342
- const director = roleModel && this._llmDirector ? this._directorFor( roleModel ) : this._llmDirector
343
448
  return this._facetSupervisor.spawn( {
449
+ ...( key ? { key } : {} ),
344
450
  bus: this._bus,
345
- llmDirector: director,
451
+ llmDirector: this._llmDirector,
346
452
  stateRef: this._lastStateRef,
347
453
  willId: this._willId,
348
454
  inbox: this._inbox,
@@ -358,6 +464,14 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
358
464
  } )
359
465
  }
360
466
 
467
+ /**
468
+ * The facet already attending to `key`, if one is open — without spawning.
469
+ * See FacetSupervisor.handleFor.
470
+ */
471
+ facetFor( key: string ): ExecutiveFacetHandle | undefined {
472
+ return this._facetSupervisor.handleFor( key )
473
+ }
474
+
361
475
  // ── CognitiveEngine interface ──────────────────────────────
362
476
 
363
477
  subscribes(): string[] { return [ '*' ] }
@@ -374,14 +488,28 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
374
488
  onCognitiveEvent( event: CognitiveEvent ): StateCommands | void {
375
489
  if( event.sourceEngine === this.name ) return
376
490
 
377
- // Set attention state for bandwidth allowance
378
- // One facet per ~0.3 free capacity units, floor at 1
491
+ // Spare attention scales the facet allowance within the persona's ceiling.
379
492
  if( event.type === 'attention.state.changed'){
380
493
  const p = event.payload as { freeFraction: number }
381
494
  this._facetSupervisor.setAttentionState( p.freeFraction )
382
495
  return
383
496
  }
384
497
 
498
+ // The facet legs. These MUST be dispatched here rather than through their own
499
+ // `bus.subscribe` calls: the bus keeps one subscription per engineId, and the
500
+ // orchestrator's `subscribe( engine.name, engine.subscribes(), … )` runs after
501
+ // `attachBus`, so anything registered separately is silently replaced. This
502
+ // `['*']` subscription is the engine's only live one.
503
+ if( event.type === 'executive.facet.sync'){
504
+ this._onFacetSync( event )
505
+ return
506
+ }
507
+
508
+ if( event.type === 'executive.facet.handoff'){
509
+ this._onFacetHandoff( event )
510
+ return
511
+ }
512
+
385
513
  // GWT salience-based buffer
386
514
  ( event.salience ?? 0 ) >= WORKSPACE_THRESHOLD
387
515
  && this._gatingState.salienceBuffer.push( {
@@ -404,6 +532,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
404
532
  state: ReadonlySimulationState,
405
533
  context: SimulationContext,
406
534
  ): Promise<EngineResult> {
535
+ this._currentTick = tick as unknown as number
407
536
  this._deferred.flush( state, tick as unknown as number )
408
537
  this._deferred.markReactTick( tick as unknown as number )
409
538
  // Per-tick facet pump: refresh every facet's state ref to THIS tick's frozen
@@ -411,7 +540,77 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
411
540
  // Sits at a fixed point in the serial engine order — the issue-side twin of
412
541
  // the CompletionInbox drain in Phase 2. See .TODO/FACET_REPLAY_DETERMINISM.md.
413
542
  this._facetSupervisor.pump( state )
414
- return super.react( delta, tick, state, context )
543
+
544
+ const result = await super.react( delta, tick, state, context )
545
+
546
+ // Reasoning facets occupy attention. Merged into whatever this tick already
547
+ // produced — AsyncEngine.react returns commands on every tick, reasoning or not,
548
+ // so the allocator sees the cost land the tick it is incurred.
549
+ const focus = this._facetAttentionDemands( state, tick as unknown as number )
550
+ if( focus.set.length || focus.delete.length ){
551
+ result.commands ??= {}
552
+ result.commands.set = [ ...( result.commands.set ?? [] ), ...focus.set ]
553
+ result.commands.delete = [ ...( result.commands.delete ?? [] ), ...focus.delete ]
554
+ }
555
+
556
+ return result
557
+ }
558
+
559
+ /**
560
+ * What the mind is attending to because a facet is reasoning about it, as
561
+ * `attention.demand` entities the AttentionAllocator allocates real capacity
562
+ * against (`_extractSalienceSignals` reads this type; `costPerFocus` is then
563
+ * charged against the same 100-unit budget as perceptual foci).
564
+ *
565
+ * This closes a loop that was open in one direction only: the allocator's
566
+ * `freeFraction` scaled the facet budget, but facets never appeared in
567
+ * `_activeFocus`, so holding three conversations reported exactly as much spare
568
+ * attention as holding none. The budget was being scaled by a signal blind to the
569
+ * thing it was bounding.
570
+ *
571
+ * `urgency` sits below 1 on purpose: a live conversation is a genuine claim on
572
+ * attention but must not automatically outrank every percept — the allocator sorts
573
+ * candidates by salience into `maxFoci` slots, and a facet that always won would
574
+ * starve perception. Only BUSY facets are charged; an open-but-quiet thread is one
575
+ * the mind is in, not one it is attending to.
576
+ */
577
+ private _facetAttentionDemands(
578
+ state: ReadonlySimulationState,
579
+ tick: number,
580
+ ): { set: EntityInput[]; delete: string[] } {
581
+ const busy = this._facetSupervisor.busyFacetIds()
582
+ const set: EntityInput[] = []
583
+ const live = new Set<string>()
584
+
585
+ for( const facetId of busy ){
586
+ const id = `facet-attending-${facetId}`
587
+ live.add( id )
588
+ const subject = this._facetSubjects.get( facetId )
589
+ set.push({
590
+ id,
591
+ type: 'attention.demand',
592
+ metadata: {
593
+ urgency: FACET_ATTENTION_URGENCY,
594
+ source: 'executive-facet',
595
+ facetId,
596
+ ...( subject?.entityId ? { subjectEntityId: subject.entityId } : {} ),
597
+ ...( subject?.name ? { subjectName: subject.name } : {} ),
598
+ // NOT `generatesGoal` — GoalManager turns flagged demands into goals, and
599
+ // "I am talking to someone" is a state, not something to pursue.
600
+ tick,
601
+ },
602
+ })
603
+ }
604
+
605
+ // Release the attention a facet held once it stops reasoning (or is reaped).
606
+ const del: string[] = []
607
+ for( const [ id, e ] of state.entities )
608
+ if( e.type === 'attention.demand'
609
+ && ( e.metadata as Record<string, unknown> | undefined )?.['source'] === 'executive-facet'
610
+ && !live.has( id ) )
611
+ del.push( id )
612
+
613
+ return { set, delete: del }
415
614
  }
416
615
 
417
616
  protected override shouldAct(
@@ -425,6 +624,12 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
425
624
  this._summarizerRestored = true
426
625
  }
427
626
 
627
+ // Restore the deliberation cache from state on first tick (parallels the summary)
628
+ if( this._cache && !this._cacheRestored ){
629
+ this._restoreDeliberationCache( state )
630
+ this._cacheRestored = true
631
+ }
632
+
428
633
  // Read runtime config overrides from state
429
634
  const rtConfig = readRuntimeConfig( state, {
430
635
  executiveInterval: this._executiveInterval,
@@ -433,21 +638,37 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
433
638
  this._gatingState.executiveInterval = rtConfig.executiveInterval
434
639
  this._gatingState.cooldownTicks = rtConfig.cooldownTicks
435
640
 
436
- // Initialize LLM directors if not yet done (requires willId). One director
437
- // per distinct role model; roles that share a model share the instance.
641
+ // Initialize the LLM director if not yet done (requires willId). One per
642
+ // Will every role reaches its model through the router.
438
643
  if( !this._llmDirector && this._willId ){
439
- const execModel = this._models.executive ?? process.env.WILL_LLM_MODEL
440
- ?? defaultModelFor( ( this._llm?.provider ?? process.env.WILL_LLM_PROVIDER ?? 'anthropic') as LLMProvider )
441
- this._llmDirector = this._directorFor( execModel )
442
- // The summarizer runs its role's model (falls back to executive) with the
443
- // same provider, session logging and token tracking.
444
- this._summarizer?.attachLLMDirector( this._directorFor( this._models.summarizer ?? execModel ) )
644
+ // No default model. The engine used to fall back to a Claude id for every
645
+ // provider but GLM, which meant a misconfigured Will asked the wrong
646
+ // vendor for the wrong model and failed at the first tick with a 404
647
+ // instead of at construction with a sentence.
648
+ //
649
+ // Two cases legitimately have no credentials and are exempt: a test-mode
650
+ // Will (never reaches a network — demanding a key would break the whole
651
+ // point of the no-key quickstart) and a replay (completions are re-fed
652
+ // from the tape, which carries the model that actually served them). The
653
+ // sentinel is what a mock run records, so the tape still says plainly
654
+ // that nothing real answered.
655
+ const defaultModel = this._modelId ?? process.env.WILL_LLM_MODEL
656
+ ?? ( this._noLiveCalls() ? MOCK_MODEL : undefined )
657
+ if( !defaultModel )
658
+ throw new Error(
659
+ 'No LLM model configured. Set one on the Will (llm.model) or in the ' +
660
+ 'environment (WILL_LLM_MODEL) — the engine carries no default.'
661
+ )
662
+ this._llmDirector = this._buildDirector( defaultModel )
663
+ // The summarizer shares that director. Its calls tag themselves
664
+ // `category: 'summarizer'`, which is what a configured summarizer role
665
+ // compiles to — so it still gets its own model, chosen per call.
666
+ this._summarizer?.attachLLMDirector( this._llmDirector )
445
667
  }
446
668
 
447
669
  // Evaluate gating
448
670
  const gatingDeps: GatingDependencies = {
449
671
  generativeModel: this._generativeModel,
450
- pendingMessages: this._messageQueue.pendingMessages,
451
672
  hasPendingWork: this.hasPendingWork
452
673
  }
453
674
 
@@ -489,7 +710,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
489
710
  stream: IntermediateStream,
490
711
  ): Promise<unknown> {
491
712
  this._lastStateRef = state
492
- this._messageQueue.pendingCallStartTick = state.tick
493
713
 
494
714
  // Mark which buffer entries this cycle consumes. Anything pushed after this
495
715
  // point arrives during the LLM call and was never part of the broadcast, so
@@ -499,6 +719,47 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
499
719
  // Update all facets with the latest state reference
500
720
  this._facetSupervisor.broadcastStateRef( state )
501
721
 
722
+ // ── DeliberationCache fast path (optional) ───────────────
723
+ // A learned interpolation of past executive outputs. When a highly similar,
724
+ // highly competent precedent exists we skip the LLM entirely. R2-safe: the
725
+ // fingerprint, retrieval and composition are pure functions of the frozen
726
+ // state and the cache's own snapshotted contents. The mandatory bookkeeping
727
+ // above (buffer marking, facet broadcast) has already run, so a cache hit and
728
+ // a slow tick leave the facet/buffer discipline in the same state.
729
+ this._pendingVerify = null
730
+ if( this._cache ){
731
+ const cacheTick = footprint.tickObserved as unknown as number
732
+ this._cache.decay()
733
+ const fp = extractFingerprint( state )
734
+ const cacheResult = this._cache.retrieve( fp, cacheTick )
735
+ this._lastCacheHit = cacheResult.hit
736
+ this._lastCacheConfidence = cacheResult.confidence
737
+ this._lastCacheNeighborCount = cacheResult.neighbors.length
738
+
739
+ if( cacheResult.hit && cacheResult.output ){
740
+ if( !this._cache.shouldVerify() ){
741
+ // FAST PATH — return the composed output; this tick spends no tokens.
742
+ stream.report('executive_complete', {
743
+ actionCount: cacheResult.output.actions.length,
744
+ planCount: 0,
745
+ newBeliefCount: cacheResult.output.newBeliefs?.length ?? 0,
746
+ hasIntrospection: false,
747
+ hasNarrative: false,
748
+ } )
749
+ logger.info(
750
+ `[executive] ⚡ cache hit tick=${cacheTick} ` +
751
+ `ρ=${cacheResult.confidence.toFixed( 3 )} neighbors=${cacheResult.neighbors.length}`
752
+ )
753
+ return cacheResult.output
754
+ }
755
+ // Hit selected for verification — run the LLM and score the cache against it.
756
+ this._pendingVerify = {
757
+ fingerprint: fp,
758
+ cachedActionTypes: cacheResult.output.actions.map( a => a.type ),
759
+ }
760
+ }
761
+ }
762
+
502
763
  // Build executive context using PromptFactory's helper
503
764
  const execContext = await PromptFactory.buildFreshContext( {
504
765
  workingMemory: this._workingMemory,
@@ -532,7 +793,13 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
532
793
  priorConfidence: this._lastExecutiveOutput?.confidence ?? 0.5,
533
794
  novelty: state.metrics.get('perception.novelty') ?? 0,
534
795
  stressLoad: state.metrics.get('stress.load') ?? 0,
535
- hasPendingMessage: this._messageQueue.pendingMessages.length > 0,
796
+ // Something a conversation surfaced is waiting on the master and has not
797
+ // been dealt with. This used to read the pending-message queue, which was
798
+ // never filled — so the effort gate's `pending_reply` term was structurally
799
+ // zero for every master that has ever run. Escalations are the master's real
800
+ // version of "someone is waiting on me": the facets do the replying, the
801
+ // master owns what they escalate.
802
+ hasPendingMessage: this._escalations.size > 0,
536
803
  }, deliberateThreshold )
537
804
  stream.report('process_selected', {
538
805
  process: processSelection.process,
@@ -575,11 +842,11 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
575
842
  state,
576
843
  qualityModulation,
577
844
  epistemicUncertainty,
578
- pendingMessages: [ ...this._messageQueue.pendingMessages ],
579
- focus,
845
+ focus,
580
846
  deps: promptDeps,
581
847
  recentActionTypes: [ ...this._recentActionTypes ],
582
848
  mode: 'master',
849
+ activeConversations: this._activeConversations(),
583
850
  outputFormat: PromptFactory.buildIdeationFormatInstruction(),
584
851
  } )
585
852
  // Propose temperature scales with the creativity trait (TODO #4): a creative Will
@@ -593,7 +860,13 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
593
860
  ideationUserMessage,
594
861
  tick: state.tick,
595
862
  proposeTemperature,
596
- meta: { category: 'executive', attribute: 'master', function: 'ideation' },
863
+ meta: {
864
+ category: 'executive',
865
+ attribute: 'master',
866
+ process: 'ideation',
867
+ function: '-',
868
+ demand: processSelection.effortScore
869
+ },
597
870
  } )
598
871
  logger.info(
599
872
  `[executive] ◆ deliberate propose tick=${state.tick} ` +
@@ -613,11 +886,11 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
613
886
  state,
614
887
  qualityModulation,
615
888
  epistemicUncertainty,
616
- pendingMessages: [ ...this._messageQueue.pendingMessages ],
617
889
  focus,
618
890
  deps: promptDeps,
619
891
  recentActionTypes: [ ...this._recentActionTypes ],
620
892
  mode: 'master',
893
+ activeConversations: this._activeConversations(),
621
894
  ideationCandidates
622
895
  } )
623
896
 
@@ -631,7 +904,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
631
904
  // D2: context counts for per-tick cognitive state snapshot
632
905
  workingMemoryItems: execContext.workingMemory.length,
633
906
  goalCount: execContext.goals.length,
634
- pendingMessages: this._messageQueue.pendingMessages.length,
635
907
  beliefCount: execContext.beliefs.length,
636
908
  beliefsOmitted: execContext.beliefsOmitted,
637
909
  promptPath: this._llmDirector?.writeDebugPrompt( state.tick, systemPrompt, userMessage ) ?? ''
@@ -646,7 +918,10 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
646
918
 
647
919
  try {
648
920
  // Use streaming call when clients are connected (F3); fall back to regular call.
649
- const masterMeta = { category: 'executive', attribute: 'master', function: 'decision' }
921
+ // MODEL_ROUTING W0 the effort gate already weighed this tick's demand
922
+ // (uncertainty, prior confidence, novelty, a pending reply, stress load);
923
+ // forward it rather than inventing a second measure of the same thing.
924
+ const masterMeta: LLMCallMeta = { category: 'executive', attribute: 'master', process: 'decision', function: '-', demand: processSelection.effortScore }
650
925
  const result = this._chunkBroadcaster
651
926
  ? await this._llmDirector.callStream( systemPrompt, userMessage, state.tick, this._chunkBroadcaster, undefined, masterMeta )
652
927
  : await this._llmDirector.call( systemPrompt, userMessage, state.tick, undefined, masterMeta )
@@ -733,6 +1008,23 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
733
1008
  hasNarrative: !!executiveOutput.narrative
734
1009
  } )
735
1010
 
1011
+ // ── DeliberationCache learning (slow path) ───────────────
1012
+ // extractFingerprint is pure over the frozen `state`, so recomputing here
1013
+ // yields the identical vector used on entry — no need to thread it through.
1014
+ if( this._cache ){
1015
+ const learnTick = footprint.tickObserved as unknown as number
1016
+ if( this._pendingVerify ){
1017
+ const match = this._actionTypesMatch(
1018
+ this._pendingVerify.cachedActionTypes,
1019
+ executiveOutput.actions.map( a => a.type ),
1020
+ )
1021
+ this._cache.updateCompetence( this._pendingVerify.fingerprint, match ? 1 : 0, learnTick )
1022
+ this._pendingVerify = null
1023
+ } else {
1024
+ this._cache.learn( extractFingerprint( state ), executiveOutput, learnTick )
1025
+ }
1026
+ }
1027
+
736
1028
  return executiveOutput
737
1029
  }
738
1030
 
@@ -811,7 +1103,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
811
1103
  )
812
1104
 
813
1105
  // ── Flush pending escalation percepts ──────────────────────
814
- // Convert buffered audition.task.signal events into high-salience
1106
+ // Convert buffered executive.facet.handoff events into high-salience
815
1107
  // percept entities so Exteroception surfaces them as
816
1108
  // "## Percepts (What I Notice)" on the NEXT master cycle.
817
1109
  // The master sees them as environmental signals — not as messages to reply to.
@@ -898,12 +1190,54 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
898
1190
  } )
899
1191
 
900
1192
  // Clear processed messages
901
- this._messageQueue.clearProcessedMessages()
902
1193
 
903
- // Merge escalation percepts into final commands
904
- if( escalationPercepts.length ){
1194
+ // Merge escalation percepts into final commands, and retire the ones already
1195
+ // honoured. See _dischargedUndertakings for why this is not optional.
1196
+ const { keep, discharge } = this._reconcileUndertakings( escalationPercepts, this._lastStateRef! )
1197
+ if( keep.length ){
1198
+ commands.set ??= []
1199
+ commands.set.push( ...keep )
1200
+ }
1201
+ if( discharge.length ){
1202
+ commands.delete ??= []
1203
+ commands.delete.push( ...discharge )
1204
+ }
1205
+
1206
+ // ── Persist deliberation cache (mirrors the rolling summary) ──
1207
+ // Written each executive cycle so the learned patterns survive snapshot/
1208
+ // restore (R2). Bounded by maxPatterns; the stored output objects are only
1209
+ // ever read back, so state's deep-freeze of them is harmless.
1210
+ if( this._cache ){
905
1211
  commands.set ??= []
906
- commands.set.push( ...escalationPercepts )
1212
+ commands.set.push( {
1213
+ id: 'executive-deliberation-cache',
1214
+ type: 'executive.cache',
1215
+ metadata: { snapshot: this._cache.snapshot() },
1216
+ } )
1217
+
1218
+ // Surface cache state the R2-safe way: metrics ride the returned
1219
+ // StateCommands (never a frozen-state mutation), and hit/miss events
1220
+ // publish from this committed path so faculties can react to how
1221
+ // automatic the Will is becoming.
1222
+ const total = this._cache.hitCount + this._cache.missCount
1223
+ commands.metrics ??= []
1224
+ commands.metrics.push(
1225
+ [ 'cache.hit', this._lastCacheHit ? 1 : 0 ],
1226
+ [ 'cache.confidence', this._lastCacheConfidence ],
1227
+ [ 'cache.hit_rate', total > 0 ? this._cache.hitCount / total : 0 ],
1228
+ [ 'cache.size', this._cache.size ],
1229
+ )
1230
+ this._bus?.publish( this._lastCacheHit
1231
+ ? {
1232
+ type: 'cache.hit', version: 1, sourceEngine: this.name,
1233
+ salience: this._lastCacheConfidence,
1234
+ payload: { confidence: this._lastCacheConfidence, neighborCount: this._lastCacheNeighborCount },
1235
+ }
1236
+ : {
1237
+ type: 'cache.miss', version: 1, sourceEngine: this.name,
1238
+ salience: 0.3,
1239
+ payload: { confidence: this._lastCacheConfidence },
1240
+ } )
907
1241
  }
908
1242
 
909
1243
  // Track entities modified
@@ -923,122 +1257,287 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
923
1257
 
924
1258
  // ── Private helpers ────────────────────────────────────────
925
1259
 
926
- private _ensureFacetSyncSubscription(): void {
927
- if( this._facetSyncSubscribed || !this._bus ) return
928
- this._facetSyncSubscribed = true
929
-
930
- // ── executive.facet.sync ─────────────────────────────────────
931
- // Facet sync events enter the salience buffer so the master
932
- // can re-evaluate when enough facet activity accumulates.
933
- this._bus.subscribe(
934
- this.name,
935
- [ 'executive.facet.sync' ],
936
- ( event ) => {
937
- const payload = event.payload as {
938
- facetId?: string
939
- reasoning?: string
940
- confidence?: number
941
- tick?: number
942
- }
1260
+ /**
1261
+ * The people the mind is in conversation with right now, newest first.
1262
+ *
1263
+ * Pruned against the supervisor's live facets on every read: a reaped facet is a
1264
+ * conversation that has ended, and a master that still believes it is mid-thread
1265
+ * with someone reasons about a table that is no longer there.
1266
+ */
1267
+ private _activeConversations(): { entityId: string; name?: string; sinceTick: number }[] {
1268
+ const live = this._facetSupervisor.liveFacetIds()
1269
+ for( const id of [ ...this._facetSubjects.keys() ] )
1270
+ if( !live.has( id ) ) this._facetSubjects.delete( id )
1271
+
1272
+ return [ ...this._facetSubjects.values() ]
1273
+ .sort( ( a, b ) => b.tick - a.tick )
1274
+ .map( s => ({ entityId: s.entityId, ...( s.name ? { name: s.name } : {} ), sinceTick: s.tick }) )
1275
+ }
943
1276
 
944
- const syntheticEvent = {
945
- id: '',
946
- type: 'executive.facet.sync',
947
- version: 1,
948
- sequenceNumber: 1,
949
- sourceEngine: `executive-facet-${payload.facetId ?? 'unknown'}`,
950
- salience: Math.max( 0.5, payload.confidence ?? 0.5 ),
951
- payload,
952
- wallTime: payload.tick as number,
953
- logicalTime: payload.tick as number
1277
+ /**
1278
+ * Facet sync — remember WHO each facet is with, and wake the master.
1279
+ *
1280
+ * Reached from `onCognitiveEvent`, NOT from its own `bus.subscribe`. The bus
1281
+ * stores one subscription per engineId (`_subscriptions.set( engineId, … )`),
1282
+ * so a second `subscribe(this.name, …)` silently REPLACES the first — and the
1283
+ * orchestrator registers `subscribe( engine.name, engine.subscribes(), )`
1284
+ * after `attachBus`, which replaced everything registered here. Two dedicated
1285
+ * handlers used to be installed at this point; the second overwrote the first
1286
+ * and the orchestrator then overwrote that, so neither ever ran. The escalation
1287
+ * leg had been dead in production for its whole life: a facet could escalate,
1288
+ * the audition engine published, and nothing was listening.
1289
+ */
1290
+ /**
1291
+ * Retire undertakings the mind has already honoured, and refuse to restate one
1292
+ * it is already carrying.
1293
+ *
1294
+ * An undertaking percept says, in the first person, "I said I would reach X and
1295
+ * nothing has gone to them yet". That sentence has to stop being true at some
1296
+ * point, and nothing made it stop. Measured on a live Will: SEVEN of them
1297
+ * accumulated in state, every one still asserting nothing had been sent, while
1298
+ * a `conversation.sent` to that person sat right beside them. She read seven
1299
+ * standing unfulfilled promises every cycle and dutifully sent the same message
1300
+ * again, five times in five minutes and once more in the next session — the
1301
+ * percept meant to stop her forgetting a promise was making her unable to
1302
+ * believe she had kept it.
1303
+ *
1304
+ * Discharged by EVIDENCE, not by a timer: a `conversation.sent` to that target,
1305
+ * written no earlier than the undertaking, means the contact happened. That
1306
+ * record is durable and snapshots with the state, so the discharge survives a
1307
+ * restart exactly as the promise does — which the tick-scoped satiation in
1308
+ * `enactionFootprint` deliberately cannot.
1309
+ *
1310
+ * It stays a decision, not an erasure. Retiring the percept removes the standing
1311
+ * claim that the words are unsent; whether to say more to that person is then an
1312
+ * ordinary competition like any other.
1313
+ */
1314
+ private _reconcileUndertakings(
1315
+ incoming: EntityInput[],
1316
+ state: ReadonlySimulationState,
1317
+ ): { keep: EntityInput[]; discharge: string[] } {
1318
+ /** Latest tick at which anything was sent to each target. */
1319
+ const contacted = new Map<string, number>()
1320
+ for( const [ , e ] of state.entities ){
1321
+ if( e.type !== 'conversation.sent') continue
1322
+ const m = e.metadata as Record<string, unknown> | undefined
1323
+ const target = typeof m?.['targetEntityId'] === 'string' ? m['targetEntityId'] as string : undefined
1324
+ if( !target ) continue
1325
+ // metadata.tick where set, else the entity's own sim-clock tick. Reading
1326
+ // metadata alone defaulted every record to 0, so `contacted >= madeAt` was
1327
+ // false for any promise made after tick 0 — this discharge never once fired.
1328
+ const at = typeof m?.['tick'] === 'number' ? m['tick'] as number
1329
+ : typeof ( e as { tick?: number } ).tick === 'number' ? ( e as { tick?: number } ).tick as number
1330
+ : 0
1331
+ contacted.set( target, Math.max( contacted.get( target ) ?? 0, at ) )
1332
+ }
1333
+
1334
+ /** An undertaking is spent once we have reached its target since making it. */
1335
+ const honoured = ( target: string | undefined, madeAt: number ): boolean =>
1336
+ !!target && ( contacted.get( target ) ?? -1 ) >= madeAt
1337
+
1338
+ const undertakingOf = ( m: Record<string, unknown> | undefined ) =>
1339
+ m?.['category'] === 'undertaking'
1340
+ ? {
1341
+ target: typeof m['undertakingTarget'] === 'string' ? m['undertakingTarget'] as string : undefined,
1342
+ madeAt: typeof m['tick'] === 'number' ? m['tick'] as number : 0,
954
1343
  }
1344
+ : undefined
1345
+
1346
+ const discharge: string[] = []
1347
+ /** Targets we are already carrying a live, unhonoured undertaking toward. */
1348
+ const carrying = new Set<string>()
1349
+
1350
+ for( const [ id, e ] of state.entities ){
1351
+ if( e.type !== 'percept') continue
1352
+ const u = undertakingOf( e.metadata as Record<string, unknown> | undefined )
1353
+ if( !u ) continue
1354
+
1355
+ // No target ⇒ it predates the field this reconciliation keys on, so it can
1356
+ // never be matched against a contact and would assert "nothing has gone to
1357
+ // them yet" for the rest of the mind's life. Seven of these were found in a
1358
+ // live snapshot, each one re-read every cycle. Undischargeable is strictly
1359
+ // worse than gone.
1360
+ if( !u.target ){ discharge.push( id ); continue }
1361
+
1362
+ if( honoured( u.target, u.madeAt ) ) discharge.push( id )
1363
+ else carrying.add( u.target )
1364
+ }
955
1365
 
956
- this._gatingState.salienceBuffer.push( {
957
- event: syntheticEvent,
958
- tick: payload.tick ?? 0
959
- } )
1366
+ // Drop an incoming restatement of something already honoured or already held.
1367
+ // Duplicates were how seven of these piled up: each master cycle drained a
1368
+ // fresh copy of a promise that could never be marked kept.
1369
+ const keep = incoming.filter( p => {
1370
+ const u = undertakingOf( p.metadata as Record<string, unknown> | undefined )
1371
+ if( !u?.target ) return true
1372
+ if( honoured( u.target, u.madeAt ) ) return false
1373
+ if( carrying.has( u.target ) ) return false
1374
+ carrying.add( u.target )
1375
+ return true
1376
+ } )
960
1377
 
961
- logger.info(
962
- `[executive] master received facet sync from ${payload.facetId} ` +
963
- `(confidence=${payload.confidence?.toFixed( 2 )})`
964
- )
965
- }
1378
+ if( discharge.length )
1379
+ logger.info(`[executive] ${ discharge.length } undertaking(s) discharged the contact was made`)
1380
+
1381
+ return { keep, discharge }
1382
+ }
1383
+
1384
+ private _onFacetSync( event: CognitiveEvent ): void {
1385
+ const payload = event.payload as {
1386
+ facetId?: string
1387
+ reasoning?: string
1388
+ confidence?: number
1389
+ tick?: number
1390
+ subjectEntityId?: string
1391
+ subjectName?: string
1392
+ }
1393
+
1394
+ // Remember WHO this facet is with, not just that it reported.
1395
+ if( payload.facetId && payload.subjectEntityId )
1396
+ this._facetSubjects.set( payload.facetId, {
1397
+ entityId: payload.subjectEntityId,
1398
+ ...( payload.subjectName ? { name: payload.subjectName } : {} ),
1399
+ tick: payload.tick ?? this._lastExecutiveTick ?? 0,
1400
+ })
1401
+
1402
+ // Unconditional (not gated on WORKSPACE_THRESHOLD like ordinary traffic):
1403
+ // a facet reporting back is the master's own attention returning, and the
1404
+ // point of the spike is that it re-evaluates rather than waiting out its
1405
+ // interval.
1406
+ this._gatingState.salienceBuffer.push({
1407
+ event,
1408
+ tick: payload.tick ?? event.logicalTime ?? 0,
1409
+ })
1410
+
1411
+ logger.info(
1412
+ `[executive] master received facet sync from ${payload.facetId}` +
1413
+ ( payload.subjectName || payload.subjectEntityId
1414
+ ? ` (with ${payload.subjectName ?? payload.subjectEntityId})` : '') +
1415
+ ` (confidence=${payload.confidence?.toFixed( 2 )})`
966
1416
  )
1417
+ }
967
1418
 
968
- // ── audition.task.signal ─────────────────────────────────────
969
- // Published by AuditionEngine when a conversation facet emits the
970
- // 'escalate' action type signalling that the conversation revealed
971
- // a task requiring the master's cognitive machinery (plan creation,
972
- // goal reprioritization, identity reflection).
973
- //
974
- // IMPORTANT master stays out of the reply path entirely:
975
- // The conversation facet has already sent (or will send) the
976
- // acknowledgement to the user ("Got it, I'll get started on that.").
977
- // The master's job is purely cognitive: create a [PLANS] block,
978
- // update goals, or reflect. Any follow-up communication to the user
979
- // flows through plan step execution (effector: 'text') via
980
- // ActionExecutor ProactiveCommunicator NEVER via [REPLY].
1419
+ /**
1420
+ * A focused part of me surfaced something the singular seat owns — work to plan
1421
+ * (`escalation`) or an intention toward a third party (`undertaking`).
1422
+ *
1423
+ * ONE handler for every facet type. This was `_onAuditionTaskSignal`, listening
1424
+ * on a topic named for one sense engine and typed with one sense engine's nouns
1425
+ * (`entityId`, `threadId`), which meant a planning, supervision or deliberation
1426
+ * facet had no way to hand anything up at all. See EscalationBuffer for the full
1427
+ * rationale; new kinds go in `HandoffBody`, not in a new topic and a new handler
1428
+ * beside this one.
1429
+ *
1430
+ * Master stays out of the reply path entirely:
1431
+ * • The facet has already said (or will say) whatever the person in front of
1432
+ * it needed to hear.
1433
+ * • The master's job is purely cognitive: create a [PLANS] block, update
1434
+ * goals, reflect, or decide whether it still means to make that contact.
1435
+ * Any follow-up communication flows through the agency competition —
1436
+ * NEVER via [REPLY].
1437
+ *
1438
+ * Buffered rather than written directly: state is read-only here, so
1439
+ * `EscalationBuffer.drainToPercepts()` emits it as a StateCommand on the next
1440
+ * master cycle, where Exteroception surfaces it under "## Percepts (What I Notice)".
1441
+ */
1442
+ private _onFacetHandoff( event: CognitiveEvent ): void {
1443
+ const payload = event.payload as {
1444
+ facetId?: string
1445
+ subjectEntityId?: string
1446
+ subjectName?: string
1447
+ threadId?: string
1448
+ confidence?: number
1449
+ tick?: number
1450
+ body: HandoffBody
1451
+ }
1452
+ if( !payload?.body?.kind ) return
1453
+
1454
+ // NOW, not the last time the master happened to run.
981
1455
  //
982
- // Implementation:
983
- // Write a high-salience 'percept' entity to simulation state so
984
- // Exteroception surfaces it under "## Percepts (What I Notice)".
985
- // Spike the salience buffer so the master fires soon.
986
- // Do NOT push into _messageQueue.pendingMessages that would
987
- // cause the master to produce a [REPLY], creating a duplicate
988
- // message and breaking the facet/master communication boundary.
989
- this._bus.subscribe(
990
- this.name,
991
- [ 'audition.task.signal' ],
992
- ( event ) => {
993
- const payload = event.payload as {
994
- entityId: string
995
- threadId: string
996
- reasoning: string
997
- confidence: number
998
- }
1456
+ // This was `this._lastExecutiveTick`, so an undertaking formed at tick 900 was
1457
+ // stamped 780 if that was the last master cycle. `_reconcileUndertakings`
1458
+ // discharges on "contacted at or after `madeAt`", which means a message sent at
1459
+ // tick 800 — BEFORE the promise existed retired it. Over-discharge is the safe
1460
+ // direction, which is why it never surfaced as a symptom, but it made the promise
1461
+ // unfalsifiable: it could be marked kept by something that happened first.
1462
+ const tick = payload.tick ?? this._currentTick
1463
+
1464
+ this._escalations.push({
1465
+ tick,
1466
+ ...( payload.facetId ? { facetId: payload.facetId } : {} ),
1467
+ ...( payload.subjectEntityId ? { subjectEntityId: payload.subjectEntityId } : {} ),
1468
+ ...( payload.subjectName ? { subjectName: payload.subjectName } : {} ),
1469
+ ...( payload.threadId ? { threadId: payload.threadId } : {} ),
1470
+ body: payload.body.kind === 'undertaking'
1471
+ ? { ...payload.body, reasoning: ( payload.body.reasoning ?? '').slice( 0, 400 ) }
1472
+ : { ...payload.body, reasoning: ( payload.body.reasoning ?? '').slice( 0, 400 ) },
1473
+ })
1474
+
1475
+ // Spike the salience buffer so the master fires soon rather than waiting for
1476
+ // the next scheduled interval. The master must NOT be handed the inbound as a
1477
+ // message to answer — that would make it produce a [REPLY], duplicating the
1478
+ // facet's and breaking the facet/master boundary. (The queue that once carried
1479
+ // inbound this way was removed in #114: nothing ever filled it.)
1480
+ this._gatingState.salienceBuffer.push({ event, tick })
1481
+
1482
+ const from = payload.subjectName ?? payload.subjectEntityId ?? payload.facetId ?? 'a focus'
1483
+ logger.info(
1484
+ payload.body.kind === 'undertaking'
1485
+ ? `[executive] master queued undertaking from ${from} ` +
1486
+ `→ reach ${payload.body.target} (confidence=${payload.confidence?.toFixed( 2 )})`
1487
+ : `[executive] master queued escalation percept from ${from} ` +
1488
+ `(confidence=${payload.confidence?.toFixed( 2 )})`
1489
+ )
1490
+ }
999
1491
 
1000
- // Write a percept entity so Exteroception surfaces this in
1001
- // "## Percepts (What I Notice)" — master sees it as an
1002
- // environmental signal prompting cognitive work, not a reply.
1003
- if( this._lastStateRef ){
1004
- // State is read-only here; we can't write directly.
1005
- // Instead, buffer the escalation for injection on next tick.
1006
- // The master's shouldAct() will fire due to the salience spike below.
1007
- // The actual percept write happens in onReasoningComplete() via
1008
- // a synthetic percept we store here and emit as a StateCommand.
1009
- this._escalations.push({
1010
- entityId: payload.entityId,
1011
- threadId: payload.threadId,
1012
- reasoning: payload.reasoning.slice( 0, 400 ),
1013
- tick: this._lastExecutiveTick ?? 0,
1014
- })
1015
- }
1492
+ // ── DeliberationCache wiring ───────────────────────────────
1016
1493
 
1017
- // Spike the salience buffer so the master fires soon rather than
1018
- // waiting for the next scheduled interval.
1019
- const syntheticEvent = {
1020
- id: '',
1021
- type: 'audition.task.signal',
1022
- version: 1,
1023
- sequenceNumber: 1,
1024
- sourceEngine: 'audition-engine',
1025
- salience: event.salience ?? 0.9,
1026
- payload,
1027
- wallTime: wallClock(), // telemetry field; logicalTime carries the deterministic clock
1028
- logicalTime: this._lastExecutiveTick ?? 0
1029
- }
1494
+ /** Enable the deliberation cache (off by default). Call during mind assembly. */
1495
+ enableCache( config?: DeliberationCacheConfig ): void {
1496
+ this._cache = new DeliberationCache( config )
1497
+ }
1030
1498
 
1031
- this._gatingState.salienceBuffer.push( {
1032
- event: syntheticEvent,
1033
- tick: this._lastExecutiveTick ?? 0
1034
- } )
1499
+ /** True when the cache is active. */
1500
+ get cacheEnabled(): boolean { return this._cache !== null }
1035
1501
 
1036
- logger.info(
1037
- `[executive] master queued escalation percept from entity ${payload.entityId} ` +
1038
- `(confidence=${payload.confidence.toFixed( 2 )})`
1039
- )
1040
- }
1041
- )
1502
+ /** Telemetry snapshot for harnesses / eval. Null when disabled. */
1503
+ cacheStats(): { size: number; hits: number; misses: number } | null {
1504
+ if( !this._cache ) return null
1505
+ return { size: this._cache.size, hits: this._cache.hitCount, misses: this._cache.missCount }
1506
+ }
1507
+
1508
+ /**
1509
+ * Reafference hook — update cache competence from a confirmed action outcome.
1510
+ * Optional, layered on top of the inline verify loop. Reward follows the
1511
+ * research sketch: mean of (action succeeded, stress relief, goal progress).
1512
+ */
1513
+ onActionOutcome(
1514
+ state: ReadonlySimulationState,
1515
+ tick: Tick,
1516
+ success: boolean,
1517
+ stressDelta: number,
1518
+ goalProgressDelta: number,
1519
+ ): void {
1520
+ if( !this._cache ) return
1521
+ const reward = (
1522
+ ( success ? 1 : 0 ) +
1523
+ ( 1 - Math.max( 0, Math.min( 1, stressDelta ) ) ) +
1524
+ Math.max( 0, Math.min( 1, goalProgressDelta ) )
1525
+ ) / 3
1526
+ this._cache.updateCompetence( extractFingerprint( state ), reward, tick )
1527
+ }
1528
+
1529
+ private _actionTypesMatch( a: string[], b: string[] ): boolean {
1530
+ if( a.length !== b.length ) return false
1531
+ for( let i = 0; i < a.length; i++ ) if( a[ i ] !== b[ i ] ) return false
1532
+ return true
1533
+ }
1534
+
1535
+ private _restoreDeliberationCache( state: ReadonlySimulationState ): void {
1536
+ if( !this._cache ) return
1537
+ const entity = state.entities.get('executive-deliberation-cache')
1538
+ if( !entity ) return
1539
+ const snap = entity.metadata?.[ 'snapshot' ] as DeliberationCacheSnapshot | undefined
1540
+ if( snap ) this._cache.restore( snap )
1042
1541
  }
1043
1542
 
1044
1543
  private _restoreSummarizer( state: ReadonlySimulationState ): void {