@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
@@ -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,
@@ -68,14 +72,13 @@ import { buildFallbackOutput, parseResponse } from '#faculties/executive.engine/
68
72
  import { selectProcess, ideationTemperature, DELIBERATE_THRESHOLD } from '#faculties/executive.engine/effort.gate'
69
73
  import { proposeCandidates } from '#faculties/executive.engine/deliberate.reasoning'
70
74
  import { readEffectiveParams } from '#cognition/persona.prior'
71
- import { MessageQueue } from '#faculties/executive.engine/messages'
72
75
  import {
73
76
  buildStateCommands,
74
77
  publishCognitiveEvents,
75
78
  type CommandDependencies
76
79
  } from '#faculties/executive.engine/commands'
77
80
  import { DeferredEffectQueue } from '#faculties/executive.engine/deferred.effects'
78
- import { EscalationBuffer } from '#faculties/executive.engine/escalation.buffer'
81
+ import { EscalationBuffer, type HandoffBody } from '#faculties/executive.engine/escalation.buffer'
79
82
  import { FacetSupervisor } from '#faculties/executive.engine/facet.supervisor'
80
83
  import type { EngineResult } from '#core/orchestrator'
81
84
  import type { Duration } from '#core/types'
@@ -84,6 +87,17 @@ import { wallClock } from '#core/wall.clock'
84
87
  // Re-export for compatibility
85
88
  export { ExecutiveFacet, type ExecutiveFacetHandle }
86
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
+
87
101
  // ── Engine config ───────────────────────────────────────────
88
102
 
89
103
  export interface ExecutiveEngineConfig {
@@ -131,7 +145,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
131
145
  private _testMode = false
132
146
 
133
147
  // ── Message queue ──────────────────────────────────────────
134
- private _messageQueue = new MessageQueue()
135
148
 
136
149
  // ── Action diversity tracking ──────────────────────────────
137
150
  private _recentActionTypes: string[] = []
@@ -146,6 +159,15 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
146
159
  private _lastExecutiveOutput: ExecutiveOutputFull | null = null
147
160
  private _lastExecutiveTick: number = -100
148
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
+
149
171
  // ── Injected dependencies ──────────────────────────────────
150
172
  private _willId: string | null = null
151
173
  /**
@@ -175,7 +197,15 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
175
197
  // engine keeps only the bus-subscription guard, since those subscriptions
176
198
  // feed the gating salience buffer and are shared with the escalation path.
177
199
  private readonly _facetSupervisor = new FacetSupervisor()
178
- 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 }>()
179
209
 
180
210
  // ── Cognitive models ───────────────────────────────────────
181
211
  private readonly _model = new GenerativeModel()
@@ -187,6 +217,17 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
187
217
  // ── Last state reference (for onReasoningComplete and facets) ─
188
218
  private _lastStateRef: ReadonlySimulationState | null = null
189
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
+
190
231
  // ── Deferred manager side-effects (FN11) ───────────────────
191
232
  // Commit-gated queue for the mirroring manager writes returned by
192
233
  // buildStateCommands. The queue runs them only after the orchestrator
@@ -231,8 +272,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
231
272
  goallessTickCount: 0,
232
273
  lowValenceTickCount: 0
233
274
  }
234
-
235
- this._ensureFacetSyncSubscription()
236
275
  }
237
276
 
238
277
  // ── Dependency injection ───────────────────────────────────
@@ -256,7 +295,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
256
295
  /** Called by CognitiveOrchestrator.addEngine() — injects the shared bus. */
257
296
  attachBus( bus: CognitiveBus ): void {
258
297
  this._bus = bus
259
- this._ensureFacetSyncSubscription()
260
298
  }
261
299
 
262
300
  /**
@@ -393,12 +431,22 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
393
431
  * spawn site sets a focus whose `function` matches its role — so the routed
394
432
  * answer is the same one, decided later and from the work itself.
395
433
  */
396
- spawnFacet( role?: 'deliberation' | 'conversation' | 'outreach' | 'supervision'): { attention: 'available' | 'full', handle?: ExecutiveFacetHandle } {
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 } {
397
444
  void role
398
445
  // Delegate to FacetSupervisor (R5-g-3), passing the current engine
399
446
  // attachments. The supervisor owns the registry + attention budget and
400
447
  // performs the throw-checks on bus / director / state ref.
401
448
  return this._facetSupervisor.spawn( {
449
+ ...( key ? { key } : {} ),
402
450
  bus: this._bus,
403
451
  llmDirector: this._llmDirector,
404
452
  stateRef: this._lastStateRef,
@@ -416,6 +464,14 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
416
464
  } )
417
465
  }
418
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
+
419
475
  // ── CognitiveEngine interface ──────────────────────────────
420
476
 
421
477
  subscribes(): string[] { return [ '*' ] }
@@ -432,14 +488,28 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
432
488
  onCognitiveEvent( event: CognitiveEvent ): StateCommands | void {
433
489
  if( event.sourceEngine === this.name ) return
434
490
 
435
- // Set attention state for bandwidth allowance
436
- // One facet per ~0.3 free capacity units, floor at 1
491
+ // Spare attention scales the facet allowance within the persona's ceiling.
437
492
  if( event.type === 'attention.state.changed'){
438
493
  const p = event.payload as { freeFraction: number }
439
494
  this._facetSupervisor.setAttentionState( p.freeFraction )
440
495
  return
441
496
  }
442
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
+
443
513
  // GWT salience-based buffer
444
514
  ( event.salience ?? 0 ) >= WORKSPACE_THRESHOLD
445
515
  && this._gatingState.salienceBuffer.push( {
@@ -462,6 +532,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
462
532
  state: ReadonlySimulationState,
463
533
  context: SimulationContext,
464
534
  ): Promise<EngineResult> {
535
+ this._currentTick = tick as unknown as number
465
536
  this._deferred.flush( state, tick as unknown as number )
466
537
  this._deferred.markReactTick( tick as unknown as number )
467
538
  // Per-tick facet pump: refresh every facet's state ref to THIS tick's frozen
@@ -469,7 +540,77 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
469
540
  // Sits at a fixed point in the serial engine order — the issue-side twin of
470
541
  // the CompletionInbox drain in Phase 2. See .TODO/FACET_REPLAY_DETERMINISM.md.
471
542
  this._facetSupervisor.pump( state )
472
- 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 }
473
614
  }
474
615
 
475
616
  protected override shouldAct(
@@ -483,6 +624,12 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
483
624
  this._summarizerRestored = true
484
625
  }
485
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
+
486
633
  // Read runtime config overrides from state
487
634
  const rtConfig = readRuntimeConfig( state, {
488
635
  executiveInterval: this._executiveInterval,
@@ -522,7 +669,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
522
669
  // Evaluate gating
523
670
  const gatingDeps: GatingDependencies = {
524
671
  generativeModel: this._generativeModel,
525
- pendingMessages: this._messageQueue.pendingMessages,
526
672
  hasPendingWork: this.hasPendingWork
527
673
  }
528
674
 
@@ -564,7 +710,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
564
710
  stream: IntermediateStream,
565
711
  ): Promise<unknown> {
566
712
  this._lastStateRef = state
567
- this._messageQueue.pendingCallStartTick = state.tick
568
713
 
569
714
  // Mark which buffer entries this cycle consumes. Anything pushed after this
570
715
  // point arrives during the LLM call and was never part of the broadcast, so
@@ -574,6 +719,47 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
574
719
  // Update all facets with the latest state reference
575
720
  this._facetSupervisor.broadcastStateRef( state )
576
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
+
577
763
  // Build executive context using PromptFactory's helper
578
764
  const execContext = await PromptFactory.buildFreshContext( {
579
765
  workingMemory: this._workingMemory,
@@ -607,7 +793,13 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
607
793
  priorConfidence: this._lastExecutiveOutput?.confidence ?? 0.5,
608
794
  novelty: state.metrics.get('perception.novelty') ?? 0,
609
795
  stressLoad: state.metrics.get('stress.load') ?? 0,
610
- 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,
611
803
  }, deliberateThreshold )
612
804
  stream.report('process_selected', {
613
805
  process: processSelection.process,
@@ -650,11 +842,11 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
650
842
  state,
651
843
  qualityModulation,
652
844
  epistemicUncertainty,
653
- pendingMessages: [ ...this._messageQueue.pendingMessages ],
654
- focus,
845
+ focus,
655
846
  deps: promptDeps,
656
847
  recentActionTypes: [ ...this._recentActionTypes ],
657
848
  mode: 'master',
849
+ activeConversations: this._activeConversations(),
658
850
  outputFormat: PromptFactory.buildIdeationFormatInstruction(),
659
851
  } )
660
852
  // Propose temperature scales with the creativity trait (TODO #4): a creative Will
@@ -668,7 +860,13 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
668
860
  ideationUserMessage,
669
861
  tick: state.tick,
670
862
  proposeTemperature,
671
- meta: { category: 'executive', attribute: 'master', function: 'ideation', demand: processSelection.effortScore },
863
+ meta: {
864
+ category: 'executive',
865
+ attribute: 'master',
866
+ process: 'ideation',
867
+ function: '-',
868
+ demand: processSelection.effortScore
869
+ },
672
870
  } )
673
871
  logger.info(
674
872
  `[executive] ◆ deliberate propose tick=${state.tick} ` +
@@ -688,11 +886,11 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
688
886
  state,
689
887
  qualityModulation,
690
888
  epistemicUncertainty,
691
- pendingMessages: [ ...this._messageQueue.pendingMessages ],
692
889
  focus,
693
890
  deps: promptDeps,
694
891
  recentActionTypes: [ ...this._recentActionTypes ],
695
892
  mode: 'master',
893
+ activeConversations: this._activeConversations(),
696
894
  ideationCandidates
697
895
  } )
698
896
 
@@ -706,7 +904,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
706
904
  // D2: context counts for per-tick cognitive state snapshot
707
905
  workingMemoryItems: execContext.workingMemory.length,
708
906
  goalCount: execContext.goals.length,
709
- pendingMessages: this._messageQueue.pendingMessages.length,
710
907
  beliefCount: execContext.beliefs.length,
711
908
  beliefsOmitted: execContext.beliefsOmitted,
712
909
  promptPath: this._llmDirector?.writeDebugPrompt( state.tick, systemPrompt, userMessage ) ?? ''
@@ -724,7 +921,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
724
921
  // MODEL_ROUTING W0 — the effort gate already weighed this tick's demand
725
922
  // (uncertainty, prior confidence, novelty, a pending reply, stress load);
726
923
  // forward it rather than inventing a second measure of the same thing.
727
- const masterMeta: LLMCallMeta = { category: 'executive', attribute: 'master', function: 'decision', demand: processSelection.effortScore }
924
+ const masterMeta: LLMCallMeta = { category: 'executive', attribute: 'master', process: 'decision', function: '-', demand: processSelection.effortScore }
728
925
  const result = this._chunkBroadcaster
729
926
  ? await this._llmDirector.callStream( systemPrompt, userMessage, state.tick, this._chunkBroadcaster, undefined, masterMeta )
730
927
  : await this._llmDirector.call( systemPrompt, userMessage, state.tick, undefined, masterMeta )
@@ -811,6 +1008,23 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
811
1008
  hasNarrative: !!executiveOutput.narrative
812
1009
  } )
813
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
+
814
1028
  return executiveOutput
815
1029
  }
816
1030
 
@@ -889,7 +1103,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
889
1103
  )
890
1104
 
891
1105
  // ── Flush pending escalation percepts ──────────────────────
892
- // Convert buffered audition.task.signal events into high-salience
1106
+ // Convert buffered executive.facet.handoff events into high-salience
893
1107
  // percept entities so Exteroception surfaces them as
894
1108
  // "## Percepts (What I Notice)" on the NEXT master cycle.
895
1109
  // The master sees them as environmental signals — not as messages to reply to.
@@ -976,12 +1190,54 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
976
1190
  } )
977
1191
 
978
1192
  // Clear processed messages
979
- this._messageQueue.clearProcessedMessages()
980
1193
 
981
- // Merge escalation percepts into final commands
982
- 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 ){
983
1198
  commands.set ??= []
984
- commands.set.push( ...escalationPercepts )
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 ){
1211
+ commands.set ??= []
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
+ } )
985
1241
  }
986
1242
 
987
1243
  // Track entities modified
@@ -1001,122 +1257,287 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
1001
1257
 
1002
1258
  // ── Private helpers ────────────────────────────────────────
1003
1259
 
1004
- private _ensureFacetSyncSubscription(): void {
1005
- if( this._facetSyncSubscribed || !this._bus ) return
1006
- this._facetSyncSubscribed = true
1007
-
1008
- // ── executive.facet.sync ─────────────────────────────────────
1009
- // Facet sync events enter the salience buffer so the master
1010
- // can re-evaluate when enough facet activity accumulates.
1011
- this._bus.subscribe(
1012
- this.name,
1013
- [ 'executive.facet.sync' ],
1014
- ( event ) => {
1015
- const payload = event.payload as {
1016
- facetId?: string
1017
- reasoning?: string
1018
- confidence?: number
1019
- tick?: number
1020
- }
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
+ }
1021
1276
 
1022
- const syntheticEvent = {
1023
- id: '',
1024
- type: 'executive.facet.sync',
1025
- version: 1,
1026
- sequenceNumber: 1,
1027
- sourceEngine: `executive-facet-${payload.facetId ?? 'unknown'}`,
1028
- salience: Math.max( 0.5, payload.confidence ?? 0.5 ),
1029
- payload,
1030
- wallTime: payload.tick as number,
1031
- 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,
1032
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
+ }
1033
1365
 
1034
- this._gatingState.salienceBuffer.push( {
1035
- event: syntheticEvent,
1036
- tick: payload.tick ?? 0
1037
- } )
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
+ } )
1038
1377
 
1039
- logger.info(
1040
- `[executive] master received facet sync from ${payload.facetId} ` +
1041
- `(confidence=${payload.confidence?.toFixed( 2 )})`
1042
- )
1043
- }
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 )})`
1044
1416
  )
1417
+ }
1045
1418
 
1046
- // ── audition.task.signal ─────────────────────────────────────
1047
- // Published by AuditionEngine when a conversation facet emits the
1048
- // 'escalate' action type signalling that the conversation revealed
1049
- // a task requiring the master's cognitive machinery (plan creation,
1050
- // goal reprioritization, identity reflection).
1051
- //
1052
- // IMPORTANT master stays out of the reply path entirely:
1053
- // The conversation facet has already sent (or will send) the
1054
- // acknowledgement to the user ("Got it, I'll get started on that.").
1055
- // The master's job is purely cognitive: create a [PLANS] block,
1056
- // update goals, or reflect. Any follow-up communication to the user
1057
- // flows through plan step execution (effector: 'text') via
1058
- // 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.
1059
1455
  //
1060
- // Implementation:
1061
- // Write a high-salience 'percept' entity to simulation state so
1062
- // Exteroception surfaces it under "## Percepts (What I Notice)".
1063
- // Spike the salience buffer so the master fires soon.
1064
- // Do NOT push into _messageQueue.pendingMessages that would
1065
- // cause the master to produce a [REPLY], creating a duplicate
1066
- // message and breaking the facet/master communication boundary.
1067
- this._bus.subscribe(
1068
- this.name,
1069
- [ 'audition.task.signal' ],
1070
- ( event ) => {
1071
- const payload = event.payload as {
1072
- entityId: string
1073
- threadId: string
1074
- reasoning: string
1075
- confidence: number
1076
- }
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
+ }
1077
1491
 
1078
- // Write a percept entity so Exteroception surfaces this in
1079
- // "## Percepts (What I Notice)" — master sees it as an
1080
- // environmental signal prompting cognitive work, not a reply.
1081
- if( this._lastStateRef ){
1082
- // State is read-only here; we can't write directly.
1083
- // Instead, buffer the escalation for injection on next tick.
1084
- // The master's shouldAct() will fire due to the salience spike below.
1085
- // The actual percept write happens in onReasoningComplete() via
1086
- // a synthetic percept we store here and emit as a StateCommand.
1087
- this._escalations.push({
1088
- entityId: payload.entityId,
1089
- threadId: payload.threadId,
1090
- reasoning: payload.reasoning.slice( 0, 400 ),
1091
- tick: this._lastExecutiveTick ?? 0,
1092
- })
1093
- }
1492
+ // ── DeliberationCache wiring ───────────────────────────────
1094
1493
 
1095
- // Spike the salience buffer so the master fires soon rather than
1096
- // waiting for the next scheduled interval.
1097
- const syntheticEvent = {
1098
- id: '',
1099
- type: 'audition.task.signal',
1100
- version: 1,
1101
- sequenceNumber: 1,
1102
- sourceEngine: 'audition-engine',
1103
- salience: event.salience ?? 0.9,
1104
- payload,
1105
- wallTime: wallClock(), // telemetry field; logicalTime carries the deterministic clock
1106
- logicalTime: this._lastExecutiveTick ?? 0
1107
- }
1494
+ /** Enable the deliberation cache (off by default). Call during mind assembly. */
1495
+ enableCache( config?: DeliberationCacheConfig ): void {
1496
+ this._cache = new DeliberationCache( config )
1497
+ }
1108
1498
 
1109
- this._gatingState.salienceBuffer.push( {
1110
- event: syntheticEvent,
1111
- tick: this._lastExecutiveTick ?? 0
1112
- } )
1499
+ /** True when the cache is active. */
1500
+ get cacheEnabled(): boolean { return this._cache !== null }
1113
1501
 
1114
- logger.info(
1115
- `[executive] master queued escalation percept from entity ${payload.entityId} ` +
1116
- `(confidence=${payload.confidence.toFixed( 2 )})`
1117
- )
1118
- }
1119
- )
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 )
1120
1541
  }
1121
1542
 
1122
1543
  private _restoreSummarizer( state: ReadonlySimulationState ): void {