@mindot/will 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +6985 -3337
  4. package/dist/index.js.map +1 -1
  5. package/dist/surface/channels/discord.d.ts +177 -0
  6. package/dist/surface/channels/discord.js +394 -0
  7. package/dist/surface/channels/discord.js.map +1 -0
  8. package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
  9. package/dist/{channels → surface/channels}/whatsapp.js +11 -5
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +5382 -1573
  12. package/dist/surface/cli.js.map +1 -0
  13. package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
  14. package/dist/{mcp → surface/mcp}/effectors.js +10 -4
  15. package/dist/surface/mcp/effectors.js.map +1 -0
  16. package/dist/{will-cS6k4uiJ.d.ts → will-evj9_vrd.d.ts} +6658 -5078
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +356 -1
  19. package/src/cognition/agency/conversation.aim.ts +292 -0
  20. package/src/cognition/agency/engines/action.selector.ts +142 -5
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +200 -7
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +509 -32
  24. package/src/cognition/agency/engines/reafference.engine.ts +160 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +5 -4
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/innate.ts +96 -1
  30. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  31. package/src/cognition/agency/selection.scoring.ts +49 -0
  32. package/src/cognition/agency/settlement.ts +203 -0
  33. package/src/cognition/agency/types.ts +73 -1
  34. package/src/cognition/cache/composition.ts +232 -0
  35. package/src/cognition/cache/deliberation.cache.ts +219 -0
  36. package/src/cognition/cache/fingerprint.ts +120 -0
  37. package/src/cognition/cache/types.ts +105 -0
  38. package/src/cognition/config.mirror.entities.ts +108 -1
  39. package/src/cognition/event.schemas.ts +22 -8
  40. package/src/cognition/faculties/affective.blender.ts +21 -4
  41. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  42. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  43. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  44. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  45. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  46. package/src/cognition/faculties/executive.engine/context.ts +106 -46
  47. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  48. package/src/cognition/faculties/executive.engine/engine.ts +714 -136
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +230 -45
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +320 -65
  51. package/src/cognition/faculties/executive.engine/facet.ts +81 -26
  52. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  53. package/src/cognition/faculties/executive.engine/parser.ts +120 -38
  54. package/src/cognition/faculties/executive.engine/prompt.factory.ts +331 -38
  55. package/src/cognition/faculties/executive.engine/types.ts +84 -12
  56. package/src/cognition/faculties/exteroception.ts +99 -98
  57. package/src/cognition/faculties/goal.manager.ts +144 -16
  58. package/src/cognition/faculties/known.entity.tracker.ts +287 -29
  59. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  60. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  61. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  62. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  63. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  64. package/src/cognition/faculties/self.model.updater.ts +19 -12
  65. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  66. package/src/cognition/faculties/social.perception.ts +47 -3
  67. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  68. package/src/cognition/faculties/working.memory.ts +16 -21
  69. package/src/cognition/identity.entity.ts +205 -0
  70. package/src/cognition/index.ts +7 -0
  71. package/src/cognition/instruction.handler.ts +1 -1
  72. package/src/cognition/memory/vector.adapter.ts +12 -3
  73. package/src/cognition/memory/vector.embedder.ts +45 -4
  74. package/src/cognition/percept.entity.ts +122 -0
  75. package/src/cognition/persona.prior.ts +6 -0
  76. package/src/cognition/sense.boundary.ts +176 -0
  77. package/src/cognition/senses/audition.engine/engine.ts +492 -53
  78. package/src/cognition/senses/base.sense.engine.ts +105 -7
  79. package/src/cognition/senses/index.ts +83 -13
  80. package/src/cognition/senses/provenance.ts +128 -0
  81. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  82. package/src/cognition/social.identity.ts +296 -0
  83. package/src/cognition/utilities/token.tracker.ts +94 -10
  84. package/src/core/orchestrator.ts +51 -0
  85. package/src/index.ts +39 -5
  86. package/src/llm/gate.ts +48 -0
  87. package/src/llm/index.ts +127 -33
  88. package/src/llm/routing.ts +6 -0
  89. package/src/llm/summarizer.ts +1 -1
  90. package/src/llm/wire.contracts.ts +57 -0
  91. package/src/pma/index.ts +68 -54
  92. package/src/stem/assembly.audit.ts +1 -0
  93. package/src/stem/guards/identity.coherence.ts +1 -1
  94. package/src/stem/index.ts +171 -23
  95. package/src/stem/mind.ts +198 -57
  96. package/src/stem/policy/arbiter.ts +10 -0
  97. package/src/stem/profiles/built-in.ts +7 -0
  98. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  99. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  100. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  101. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  102. package/src/{profiles → stem/profiles}/index.ts +1 -1
  103. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  104. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  105. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  106. package/src/stem/tracts/effector/types.ts +110 -0
  107. package/src/stem/tracts/effector.controller.ts +112 -401
  108. package/src/stem/tracts/outbox.controller.ts +30 -17
  109. package/src/stem/tracts/outbox.writer.ts +40 -2
  110. package/src/stem/tracts/sensory.controller.ts +6 -6
  111. package/src/stem/tracts/session.logger.ts +6 -1
  112. package/src/stem/tracts/transport/types.ts +20 -1
  113. package/src/stem/tracts/transport.controller.ts +33 -9
  114. package/src/surface/channels/discord.ts +609 -0
  115. package/src/{channels → surface/channels}/roster.ts +1 -1
  116. package/src/surface/channels/types.ts +136 -0
  117. package/src/{channels → surface/channels}/whatsapp.ts +22 -10
  118. package/src/{cli.ts → surface/cli.ts} +14 -9
  119. package/src/{host → surface/host}/boot.ts +3 -3
  120. package/src/{host → surface/host}/utterances.ts +2 -2
  121. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  122. package/src/{mcp → surface/mcp}/server.ts +19 -5
  123. package/src/{sdk → surface/sdk}/will.ts +194 -33
  124. package/src/{serve → surface/serve}/server.ts +11 -4
  125. package/src/types.ts +24 -3
  126. package/dist/channels/discord.d.ts +0 -69
  127. package/dist/channels/discord.js +0 -193
  128. package/dist/channels/discord.js.map +0 -1
  129. package/dist/channels/whatsapp.js.map +0 -1
  130. package/dist/cli.js.map +0 -1
  131. package/dist/mcp/effectors.js.map +0 -1
  132. package/src/channels/discord.ts +0 -214
  133. package/src/channels/types.ts +0 -46
  134. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
  135. package/src/extensions/livestream.ext.ts +0 -570
  136. package/src/extensions/time.ext.ts +0 -339
  137. package/src/profiles/built-in.ts +0 -7
  138. package/src/runners/coherence.runner.ts +0 -49
  139. package/src/runners/outreach.runner.ts +0 -155
  140. package/src/runners/social.runner.ts +0 -179
  141. package/src/runners/thin-shim.runner.ts +0 -205
  142. /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
@@ -18,15 +18,20 @@
18
18
  */
19
19
 
20
20
  import { logger } from '#core/logger'
21
+ import { readAliases, canonicalOf } from '#cognition/social.identity'
21
22
  import type { SessionLogger } from '#stem/tracts/session.logger'
22
23
  import type {
23
24
  Tick,
24
25
  SimulationContext,
25
26
  ReadonlySimulationState,
26
27
  ReasoningFootprint,
27
- StateCommands
28
+ StateCommands,
29
+ EntityInput
28
30
  } from '#core/types'
29
31
  import { AsyncEngine } from '#core/async.engine'
32
+ import {
33
+ actionRecordEntity, staleActionRecordIds, type ActionStatus,
34
+ } from '#faculties/executive.engine/action.record'
30
35
  import type { IntermediateStream } from '#core/async.engine'
31
36
  import type { WorkingMemory } from '#faculties/working.memory'
32
37
  import type { GoalManager, GoalState } from '#faculties/goal.manager'
@@ -42,6 +47,9 @@ import { GenerativeModel } from '#cognition/generative.model'
42
47
  import { ExecutiveSummarizer } from '#llm/summarizer'
43
48
  import { ExecutiveFacet, type ExecutiveFacetHandle } from '#faculties/executive.engine/facet'
44
49
  import type { ExecutiveOutputFull, IdeationCandidate } from '#faculties/executive.engine/types'
50
+ import { DeliberationCache } from '#cognition/cache/deliberation.cache'
51
+ import { extractFingerprint } from '#cognition/cache/fingerprint'
52
+ import type { DeliberationCacheConfig, DeliberationCacheSnapshot } from '#cognition/cache/types'
45
53
  import {
46
54
  PromptFactory,
47
55
  type PromptDependencies,
@@ -68,21 +76,29 @@ import { buildFallbackOutput, parseResponse } from '#faculties/executive.engine/
68
76
  import { selectProcess, ideationTemperature, DELIBERATE_THRESHOLD } from '#faculties/executive.engine/effort.gate'
69
77
  import { proposeCandidates } from '#faculties/executive.engine/deliberate.reasoning'
70
78
  import { readEffectiveParams } from '#cognition/persona.prior'
71
- import { MessageQueue } from '#faculties/executive.engine/messages'
72
79
  import {
73
80
  buildStateCommands,
74
81
  publishCognitiveEvents,
75
82
  type CommandDependencies
76
83
  } from '#faculties/executive.engine/commands'
77
84
  import { DeferredEffectQueue } from '#faculties/executive.engine/deferred.effects'
78
- import { EscalationBuffer } from '#faculties/executive.engine/escalation.buffer'
85
+ import { EscalationBuffer, type HandoffBody } from '#faculties/executive.engine/escalation.buffer'
79
86
  import { FacetSupervisor } from '#faculties/executive.engine/facet.supervisor'
80
87
  import type { EngineResult } from '#core/orchestrator'
81
88
  import type { Duration } from '#core/types'
82
89
  import { wallClock } from '#core/wall.clock'
83
90
 
84
- // Re-export for compatibility
85
- export { ExecutiveFacet, type ExecutiveFacetHandle }
91
+
92
+ /**
93
+ * How hard a reasoning facet pulls on attention, as an `attention.demand` urgency.
94
+ *
95
+ * Deliberately below 1: holding a live conversation is a real claim on attention,
96
+ * but the allocator sorts candidates into `maxFoci` slots by salience, and a facet
97
+ * that always outranked everything would starve perception — the mind would stop
98
+ * noticing the world whenever it was talking. At 0.7 an urgent percept still wins
99
+ * a slot; routine traffic does not.
100
+ */
101
+ const FACET_ATTENTION_URGENCY = 0.7
86
102
 
87
103
  // ── Engine config ───────────────────────────────────────────
88
104
 
@@ -131,7 +147,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
131
147
  private _testMode = false
132
148
 
133
149
  // ── Message queue ──────────────────────────────────────────
134
- private _messageQueue = new MessageQueue()
135
150
 
136
151
  // ── Action diversity tracking ──────────────────────────────
137
152
  private _recentActionTypes: string[] = []
@@ -146,6 +161,15 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
146
161
  private _lastExecutiveOutput: ExecutiveOutputFull | null = null
147
162
  private _lastExecutiveTick: number = -100
148
163
 
164
+ // ── DeliberationCache (optional fast path) ─────────────────
165
+ private _cache: DeliberationCache | null = null
166
+ private _cacheRestored = false
167
+ private _pendingVerify: { fingerprint: Float32Array; cachedActionTypes: string[] } | null = null
168
+ // Last cache outcome this cycle, surfaced as metrics/events from the committed path.
169
+ private _lastCacheHit = false
170
+ private _lastCacheConfidence = 0
171
+ private _lastCacheNeighborCount = 0
172
+
149
173
  // ── Injected dependencies ──────────────────────────────────
150
174
  private _willId: string | null = null
151
175
  /**
@@ -175,7 +199,41 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
175
199
  // engine keeps only the bus-subscription guard, since those subscriptions
176
200
  // feed the gating salience buffer and are shared with the escalation path.
177
201
  private readonly _facetSupervisor = new FacetSupervisor()
178
- private _facetSyncSubscribed = false
202
+
203
+ /**
204
+ * Who each live facet is engaged with AND what it concluded there, learned from
205
+ * `executive.facet.sync`. Keyed by facetId; the last sync wins. Rendered into
206
+ * the master's own prompt so the singular seat can reason across its
207
+ * conversations "as if they were sitting at the same table" — which it cannot
208
+ * do while it only knows facet numbers. Stale entries age out on read (see
209
+ * _activeConversations).
210
+ *
211
+ * `concluded` IS THE RETURN LEG, and it did not exist. `executive.facet.sync`
212
+ * has always carried the facet's full `reasoning`; this handler received it and
213
+ * dropped it on the floor. So the sync was one-way: the master's thinking flows
214
+ * DOWN to facets as "What I've Been Turning Over", while what a facet worked out
215
+ * came back as identity and a salience spike — that attention had been engaged,
216
+ * and with whom, never what it concluded.
217
+ *
218
+ * A facet is the same mind with a focus, not a subordinate reporting in. A mind
219
+ * that cannot read back its own thinking from where its attention has been is
220
+ * split, and the prompt comment two files over already named the consequence:
221
+ * "telling one person it has contacted another when it has not".
222
+ *
223
+ * It is kept HERE, on the engine, rather than as a percept, because a percept is
224
+ * swept after 2 ticks and a working-memory item decays below the retrieval
225
+ * threshold in about 9 — while the master's own interval is 15. Anything routed
226
+ * that way is usually gone before the master next reads. This survives to the
227
+ * next cycle by construction.
228
+ */
229
+ private _facetSubjects = new Map<string, {
230
+ entityId: string
231
+ name?: string
232
+ tick: number
233
+ concluded?: string
234
+ /** Commitments the facet declared toward a THIRD party while attending here. */
235
+ promised?: Array<{ what: string; target?: string; gist?: string; tick: number }>
236
+ }>()
179
237
 
180
238
  // ── Cognitive models ───────────────────────────────────────
181
239
  private readonly _model = new GenerativeModel()
@@ -187,6 +245,17 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
187
245
  // ── Last state reference (for onReasoningComplete and facets) ─
188
246
  private _lastStateRef: ReadonlySimulationState | null = null
189
247
 
248
+ /**
249
+ * The tick currently being processed, refreshed every react() — distinct from
250
+ * `_lastStateRef` (which tracks the REASONING tick and must not move under
251
+ * onReasoningComplete) and from `_lastExecutiveTick` (the last cycle that ran).
252
+ *
253
+ * Off-tick arrivals — a facet handoff, in particular — need to be stamped with
254
+ * when they actually happened. Using `_lastExecutiveTick` for that dated them to
255
+ * the last master cycle, which can be hundreds of ticks behind.
256
+ */
257
+ private _currentTick = 0
258
+
190
259
  // ── Deferred manager side-effects (FN11) ───────────────────
191
260
  // Commit-gated queue for the mirroring manager writes returned by
192
261
  // buildStateCommands. The queue runs them only after the orchestrator
@@ -206,6 +275,11 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
206
275
  // See EscalationBuffer for the full rationale (R5-g-2).
207
276
  private readonly _escalations = new EscalationBuffer()
208
277
 
278
+ /** First tick of an unbroken `hasPendingWork` suppression; null when the seat is free. */
279
+ private _mutedSince: Tick | null = null
280
+ /** Whether this suppression episode has already been reported. */
281
+ private _mutedWarned = false
282
+
209
283
  /** Set/clear the chunk broadcaster (called by WillManager when SSE clients connect). */
210
284
  setChunkBroadcaster( fn: (( chunk: string ) => void) | null ): void {
211
285
  this._chunkBroadcaster = fn
@@ -231,8 +305,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
231
305
  goallessTickCount: 0,
232
306
  lowValenceTickCount: 0
233
307
  }
234
-
235
- this._ensureFacetSyncSubscription()
236
308
  }
237
309
 
238
310
  // ── Dependency injection ───────────────────────────────────
@@ -256,7 +328,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
256
328
  /** Called by CognitiveOrchestrator.addEngine() — injects the shared bus. */
257
329
  attachBus( bus: CognitiveBus ): void {
258
330
  this._bus = bus
259
- this._ensureFacetSyncSubscription()
260
331
  }
261
332
 
262
333
  /**
@@ -393,12 +464,22 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
393
464
  * spawn site sets a focus whose `function` matches its role — so the routed
394
465
  * answer is the same one, decided later and from the work itself.
395
466
  */
396
- spawnFacet( role?: 'deliberation' | 'conversation' | 'outreach' | 'supervision'): { attention: 'available' | 'full', handle?: ExecutiveFacetHandle } {
467
+ spawnFacet(
468
+ role?: 'deliberation' | 'conversation' | 'outreach' | 'supervision',
469
+ /**
470
+ * What this facet is FOR — see `FacetSpawnDeps.key`. Two spawns with the same
471
+ * key get the same facet, so callers no longer each invent their own dedup
472
+ * (and `authorOutreach`, which had none, no longer opens a rival facet on a
473
+ * person the mind is already talking to).
474
+ */
475
+ key?: string,
476
+ ): { attention: 'available' | 'full', handle?: ExecutiveFacetHandle } {
397
477
  void role
398
478
  // Delegate to FacetSupervisor (R5-g-3), passing the current engine
399
479
  // attachments. The supervisor owns the registry + attention budget and
400
480
  // performs the throw-checks on bus / director / state ref.
401
481
  return this._facetSupervisor.spawn( {
482
+ ...( key ? { key: this._canonicalKey( key ) } : {} ),
402
483
  bus: this._bus,
403
484
  llmDirector: this._llmDirector,
404
485
  stateRef: this._lastStateRef,
@@ -416,11 +497,100 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
416
497
  } )
417
498
  }
418
499
 
500
+ /**
501
+ * The facet already attending to `key`, if one is open — without spawning.
502
+ * See FacetSupervisor.handleFor.
503
+ */
504
+ facetFor( key: string ): ExecutiveFacetHandle | undefined {
505
+ // Tolerant of BOTH id spaces, canonical first.
506
+ //
507
+ // A facet registered before its anchor was visible is still that person's
508
+ // open thread, and answering "nobody home" would open a second one beside it
509
+ // — the exact failure this key exists to prevent. Two ways that happens: a
510
+ // first message racing the tracker's mint, and a later name-merge that
511
+ // re-points one anchor onto another (KNOWN_ENTITY Phase 5, which is
512
+ // deliberately revisable and never destructively re-keys).
513
+ return this._facetSupervisor.handleFor( this._canonicalKey( key ) )
514
+ ?? this._facetSupervisor.handleFor( key )
515
+ }
516
+
517
+ /**
518
+ * A facet key in the PERSON's id space, not the address's.
519
+ *
520
+ * `FacetSpawnDeps.key` is `<role>:<entityId>`, and the two sides of that
521
+ * contract disagreed about which space `entityId` lives in. Audition spawns a
522
+ * conversation facet keyed by `percept.speakerEntityId` — the transport address
523
+ * the message arrived on (`discord:1019…`) — while `authorOutreach` asks for
524
+ * `conversation:<anchor>` (`ke:1sqlkux`), because the executive resolves a
525
+ * person to their anchor before willing anything at them.
526
+ *
527
+ * So the dedup this key exists for never fired for a master-willed outreach.
528
+ * Every one spawned a transient rival facet on someone the mind was already
529
+ * talking to — which is what the key was added to prevent, and what its own
530
+ * comment claims it does. Observed live: a reply and an unprompted second
531
+ * answer to the same question 27 seconds apart, the second composed by a facet
532
+ * that could not see the first.
533
+ *
534
+ * Safe to resolve here because KnownEntityTracker mints the anchor on FIRST
535
+ * sight of an address, deterministically from it (R2), so a conversation is
536
+ * keyed the same way from its first message onward.
537
+ */
538
+ private _canonicalKey( key: string ): string {
539
+ const cut = key.indexOf(':')
540
+ if( cut < 0 || !this._lastStateRef ) return key
541
+ const id = key.slice( cut + 1 )
542
+ const canon = canonicalOf( readAliases( this._lastStateRef.entities ), id )
543
+ return canon === id ? key : `${ key.slice( 0, cut ) }:${ canon }`
544
+ }
545
+
419
546
  // ── CognitiveEngine interface ──────────────────────────────
420
547
 
421
548
  subscribes(): string[] { return [ '*' ] }
422
549
  publishes(): CognitiveEventSchema[] { return [] }
423
550
 
551
+ /**
552
+ * Fold one resolved act into the mind's record of its own doing.
553
+ *
554
+ * `withheld` is kept distinct from `failed` deliberately: the mind formed the
555
+ * act and chose not to complete it, which is a judgement, not an inability.
556
+ * Collapsing them is the #123 mistake — a COO learning it was bad at speaking
557
+ * from the times it decided not to speak.
558
+ */
559
+ private _recordAction( event: CognitiveEvent ): StateCommands | void {
560
+ const p = event.payload as {
561
+ actionType?: unknown; success?: unknown; targetEntityId?: unknown
562
+ description?: unknown; planId?: unknown; tick?: unknown
563
+ } | undefined
564
+ const type = typeof p?.actionType === 'string' ? p.actionType : undefined
565
+ if( !type ) return
566
+
567
+ const status: ActionStatus = event.type === 'action.withheld' ? 'withheld'
568
+ : p?.success === false ? 'failed'
569
+ : 'completed'
570
+
571
+ const record = actionRecordEntity({
572
+ type, status,
573
+ tick: typeof p?.tick === 'number' ? p.tick : 0,
574
+ ...( typeof p?.targetEntityId === 'string' ? { targetEntityId: p.targetEntityId } : {} ),
575
+ // WHOLE. 120 here, 300 at the session log and 700 at the MCP boundary were
576
+ // three unexamined numbers cutting the same string, and the cut was the
577
+ // engine deciding how much of what a host said the mind was allowed to
578
+ // keep. What a host sends is consumed in its entirety; what the engine
579
+ // itself composes it may bound (see `PERCEPT_SUMMARY_CAP`), because there
580
+ // it is not destroying anyone's only copy.
581
+ //
582
+ // Safe to leave unbounded only because P2 split fate from facts: this
583
+ // carries a FATE, which is short by nature. The answer to a lookup leaves
584
+ // through `observation` and reaches the mind as a percept.
585
+ outcome: typeof p?.description === 'string' ? p.description : '',
586
+ ...( typeof p?.planId === 'string' ? { planId: p.planId } : {} ),
587
+ })
588
+
589
+ // Pruning happens in `react`, which has frozen state to prune against; an
590
+ // event handler sees only the event.
591
+ return { set: [ record ] }
592
+ }
593
+
424
594
  snapshot(): Record<string, unknown> {
425
595
  return {
426
596
  bufferSize: this._gatingState.salienceBuffer.length,
@@ -432,14 +602,46 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
432
602
  onCognitiveEvent( event: CognitiveEvent ): StateCommands | void {
433
603
  if( event.sourceEngine === this.name ) return
434
604
 
435
- // Set attention state for bandwidth allowance
436
- // One facet per ~0.3 free capacity units, floor at 1
605
+ // What became of what I did.
606
+ //
607
+ // the outcomes section has existed — section, builder and type —
608
+ // since it was written, fed by `context.ts` scanning `decision.record` for
609
+ // an `actionStatus` that nothing in the engine has ever set. It rendered 0
610
+ // times across every prompt a live COO received. So the mind could see what
611
+ // it had SAID and never what it had DONE, and its only other record of
612
+ // acting is `## Recent Actions`, which is built from the executive's own
613
+ // DECISIONS. A record of acting that contains only intentions cannot
614
+ // distinguish an intention from an act — and one did not, reporting a spec
615
+ // as drafted and sent when it had no effectors and had sent nothing.
616
+ //
617
+ // Written here because these events already arrive here (`subscribes()` is
618
+ // `['*']`) and the returned commands are drained onto state by the
619
+ // orchestrator. See `action.record.ts`.
620
+ if( event.type === 'action.outcome' || event.type === 'action.withheld')
621
+ return this._recordAction( event )
622
+
623
+ // Spare attention scales the facet allowance within the persona's ceiling.
437
624
  if( event.type === 'attention.state.changed'){
438
625
  const p = event.payload as { freeFraction: number }
439
626
  this._facetSupervisor.setAttentionState( p.freeFraction )
440
627
  return
441
628
  }
442
629
 
630
+ // The facet legs. These MUST be dispatched here rather than through their own
631
+ // `bus.subscribe` calls: the bus keeps one subscription per engineId, and the
632
+ // orchestrator's `subscribe( engine.name, engine.subscribes(), … )` runs after
633
+ // `attachBus`, so anything registered separately is silently replaced. This
634
+ // `['*']` subscription is the engine's only live one.
635
+ if( event.type === 'executive.facet.sync'){
636
+ this._onFacetSync( event )
637
+ return
638
+ }
639
+
640
+ if( event.type === 'executive.facet.handoff'){
641
+ this._onFacetHandoff( event )
642
+ return
643
+ }
644
+
443
645
  // GWT salience-based buffer
444
646
  ( event.salience ?? 0 ) >= WORKSPACE_THRESHOLD
445
647
  && this._gatingState.salienceBuffer.push( {
@@ -462,6 +664,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
462
664
  state: ReadonlySimulationState,
463
665
  context: SimulationContext,
464
666
  ): Promise<EngineResult> {
667
+ this._currentTick = tick as unknown as number
465
668
  this._deferred.flush( state, tick as unknown as number )
466
669
  this._deferred.markReactTick( tick as unknown as number )
467
670
  // Per-tick facet pump: refresh every facet's state ref to THIS tick's frozen
@@ -469,7 +672,85 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
469
672
  // Sits at a fixed point in the serial engine order — the issue-side twin of
470
673
  // the CompletionInbox drain in Phase 2. See .TODO/FACET_REPLAY_DETERMINISM.md.
471
674
  this._facetSupervisor.pump( state )
472
- return super.react( delta, tick, state, context )
675
+
676
+ const result = await super.react( delta, tick, state, context )
677
+
678
+ // Reasoning facets occupy attention. Merged into whatever this tick already
679
+ // produced — AsyncEngine.react returns commands on every tick, reasoning or not,
680
+ // so the allocator sees the cost land the tick it is incurred.
681
+ const focus = this._facetAttentionDemands( state, tick as unknown as number )
682
+ if( focus.set.length || focus.delete.length ){
683
+ result.commands ??= {}
684
+ result.commands.set = [ ...( result.commands.set ?? [] ), ...focus.set ]
685
+ result.commands.delete = [ ...( result.commands.delete ?? [] ), ...focus.delete ]
686
+ }
687
+
688
+ // Hold as many acts as the prompt shows and no more. Done here rather than
689
+ // in the event handler because pruning needs frozen state to prune against.
690
+ const stale = staleActionRecordIds( state.entities as never )
691
+ if( stale.length > 0 ){
692
+ result.commands ??= {}
693
+ result.commands.delete = [ ...( result.commands.delete ?? [] ), ...stale ]
694
+ }
695
+
696
+ return result
697
+ }
698
+
699
+ /**
700
+ * What the mind is attending to because a facet is reasoning about it, as
701
+ * `attention.demand` entities the AttentionAllocator allocates real capacity
702
+ * against (`_extractSalienceSignals` reads this type; `costPerFocus` is then
703
+ * charged against the same 100-unit budget as perceptual foci).
704
+ *
705
+ * This closes a loop that was open in one direction only: the allocator's
706
+ * `freeFraction` scaled the facet budget, but facets never appeared in
707
+ * `_activeFocus`, so holding three conversations reported exactly as much spare
708
+ * attention as holding none. The budget was being scaled by a signal blind to the
709
+ * thing it was bounding.
710
+ *
711
+ * `urgency` sits below 1 on purpose: a live conversation is a genuine claim on
712
+ * attention but must not automatically outrank every percept — the allocator sorts
713
+ * candidates by salience into `maxFoci` slots, and a facet that always won would
714
+ * starve perception. Only BUSY facets are charged; an open-but-quiet thread is one
715
+ * the mind is in, not one it is attending to.
716
+ */
717
+ private _facetAttentionDemands(
718
+ state: ReadonlySimulationState,
719
+ tick: number,
720
+ ): { set: EntityInput[]; delete: string[] } {
721
+ const busy = this._facetSupervisor.busyFacetIds()
722
+ const set: EntityInput[] = []
723
+ const live = new Set<string>()
724
+
725
+ for( const facetId of busy ){
726
+ const id = `facet-attending-${facetId}`
727
+ live.add( id )
728
+ const subject = this._facetSubjects.get( facetId )
729
+ set.push({
730
+ id,
731
+ type: 'attention.demand',
732
+ metadata: {
733
+ urgency: FACET_ATTENTION_URGENCY,
734
+ source: 'executive-facet',
735
+ facetId,
736
+ ...( subject?.entityId ? { subjectEntityId: subject.entityId } : {} ),
737
+ ...( subject?.name ? { subjectName: subject.name } : {} ),
738
+ // NOT `generatesGoal` — GoalManager turns flagged demands into goals, and
739
+ // "I am talking to someone" is a state, not something to pursue.
740
+ tick,
741
+ },
742
+ })
743
+ }
744
+
745
+ // Release the attention a facet held once it stops reasoning (or is reaped).
746
+ const del: string[] = []
747
+ for( const [ id, e ] of state.entities )
748
+ if( e.type === 'attention.demand'
749
+ && ( e.metadata as Record<string, unknown> | undefined )?.['source'] === 'executive-facet'
750
+ && !live.has( id ) )
751
+ del.push( id )
752
+
753
+ return { set, delete: del }
473
754
  }
474
755
 
475
756
  protected override shouldAct(
@@ -483,6 +764,12 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
483
764
  this._summarizerRestored = true
484
765
  }
485
766
 
767
+ // Restore the deliberation cache from state on first tick (parallels the summary)
768
+ if( this._cache && !this._cacheRestored ){
769
+ this._restoreDeliberationCache( state )
770
+ this._cacheRestored = true
771
+ }
772
+
486
773
  // Read runtime config overrides from state
487
774
  const rtConfig = readRuntimeConfig( state, {
488
775
  executiveInterval: this._executiveInterval,
@@ -522,7 +809,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
522
809
  // Evaluate gating
523
810
  const gatingDeps: GatingDependencies = {
524
811
  generativeModel: this._generativeModel,
525
- pendingMessages: this._messageQueue.pendingMessages,
526
812
  hasPendingWork: this.hasPendingWork
527
813
  }
528
814
 
@@ -531,6 +817,26 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
531
817
  // Always update counters, even when not activating
532
818
  updateGatingState( this._gatingState, state, tick, result.shouldActivate, result.cleanedBuffer )
533
819
 
820
+ // A reasoning pass that never settles holds this gate shut until the
821
+ // AsyncEngine's stale prune drops it 600 ticks later, and every one of those
822
+ // ticks was silent — the mind simply stopped deliberating and nothing said
823
+ // why. Warn ONCE, at the point a pass has outlived any healthy one; warning
824
+ // every tick would bury the line it exists to surface.
825
+ if( result.reason === 'hasPendingWork'){
826
+ this._mutedSince ??= tick
827
+ // Once per episode, not once at an exact tick: react() is not guaranteed to
828
+ // be called on every tick, and an `=== threshold` test that the clock steps
829
+ // over never fires at all — a silent warning about a silence.
830
+ if( !this._mutedWarned && tick - this._mutedSince >= this._executiveInterval * 3 ){
831
+ this._mutedWarned = true
832
+ logger.warn(
833
+ `[executive] no cycle for ${ tick - this._mutedSince } ticks — a reasoning pass ` +
834
+ `has not settled and is holding the seat (tick=${tick})`
835
+ )
836
+ }
837
+ }
838
+ else { this._mutedSince = null; this._mutedWarned = false }
839
+
534
840
  if( result.shouldActivate )
535
841
  logger.info(`[executive] activating — reason: ${result.reason} (tick=${tick})`)
536
842
 
@@ -564,7 +870,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
564
870
  stream: IntermediateStream,
565
871
  ): Promise<unknown> {
566
872
  this._lastStateRef = state
567
- this._messageQueue.pendingCallStartTick = state.tick
568
873
 
569
874
  // Mark which buffer entries this cycle consumes. Anything pushed after this
570
875
  // point arrives during the LLM call and was never part of the broadcast, so
@@ -574,6 +879,47 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
574
879
  // Update all facets with the latest state reference
575
880
  this._facetSupervisor.broadcastStateRef( state )
576
881
 
882
+ // ── DeliberationCache fast path (optional) ───────────────
883
+ // A learned interpolation of past executive outputs. When a highly similar,
884
+ // highly competent precedent exists we skip the LLM entirely. R2-safe: the
885
+ // fingerprint, retrieval and composition are pure functions of the frozen
886
+ // state and the cache's own snapshotted contents. The mandatory bookkeeping
887
+ // above (buffer marking, facet broadcast) has already run, so a cache hit and
888
+ // a slow tick leave the facet/buffer discipline in the same state.
889
+ this._pendingVerify = null
890
+ if( this._cache ){
891
+ const cacheTick = footprint.tickObserved as unknown as number
892
+ this._cache.decay()
893
+ const fp = extractFingerprint( state )
894
+ const cacheResult = this._cache.retrieve( fp, cacheTick )
895
+ this._lastCacheHit = cacheResult.hit
896
+ this._lastCacheConfidence = cacheResult.confidence
897
+ this._lastCacheNeighborCount = cacheResult.neighbors.length
898
+
899
+ if( cacheResult.hit && cacheResult.output ){
900
+ if( !this._cache.shouldVerify() ){
901
+ // FAST PATH — return the composed output; this tick spends no tokens.
902
+ stream.report('executive_complete', {
903
+ actionCount: cacheResult.output.actions.length,
904
+ planCount: 0,
905
+ newBeliefCount: cacheResult.output.newBeliefs?.length ?? 0,
906
+ hasIntrospection: false,
907
+ hasNarrative: false,
908
+ } )
909
+ logger.info(
910
+ `[executive] ⚡ cache hit tick=${cacheTick} ` +
911
+ `ρ=${cacheResult.confidence.toFixed( 3 )} neighbors=${cacheResult.neighbors.length}`
912
+ )
913
+ return cacheResult.output
914
+ }
915
+ // Hit selected for verification — run the LLM and score the cache against it.
916
+ this._pendingVerify = {
917
+ fingerprint: fp,
918
+ cachedActionTypes: cacheResult.output.actions.map( a => a.type ),
919
+ }
920
+ }
921
+ }
922
+
577
923
  // Build executive context using PromptFactory's helper
578
924
  const execContext = await PromptFactory.buildFreshContext( {
579
925
  workingMemory: this._workingMemory,
@@ -607,7 +953,13 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
607
953
  priorConfidence: this._lastExecutiveOutput?.confidence ?? 0.5,
608
954
  novelty: state.metrics.get('perception.novelty') ?? 0,
609
955
  stressLoad: state.metrics.get('stress.load') ?? 0,
610
- hasPendingMessage: this._messageQueue.pendingMessages.length > 0,
956
+ // Something a conversation surfaced is waiting on the master and has not
957
+ // been dealt with. This used to read the pending-message queue, which was
958
+ // never filled — so the effort gate's `pending_reply` term was structurally
959
+ // zero for every master that has ever run. Escalations are the master's real
960
+ // version of "someone is waiting on me": the facets do the replying, the
961
+ // master owns what they escalate.
962
+ hasPendingMessage: this._escalations.size > 0,
611
963
  }, deliberateThreshold )
612
964
  stream.report('process_selected', {
613
965
  process: processSelection.process,
@@ -650,11 +1002,11 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
650
1002
  state,
651
1003
  qualityModulation,
652
1004
  epistemicUncertainty,
653
- pendingMessages: [ ...this._messageQueue.pendingMessages ],
654
- focus,
1005
+ focus,
655
1006
  deps: promptDeps,
656
1007
  recentActionTypes: [ ...this._recentActionTypes ],
657
1008
  mode: 'master',
1009
+ activeConversations: this._activeConversations(),
658
1010
  outputFormat: PromptFactory.buildIdeationFormatInstruction(),
659
1011
  } )
660
1012
  // Propose temperature scales with the creativity trait (TODO #4): a creative Will
@@ -668,7 +1020,13 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
668
1020
  ideationUserMessage,
669
1021
  tick: state.tick,
670
1022
  proposeTemperature,
671
- meta: { category: 'executive', attribute: 'master', function: 'ideation', demand: processSelection.effortScore },
1023
+ meta: {
1024
+ category: 'executive',
1025
+ attribute: 'master',
1026
+ process: 'ideation',
1027
+ function: '-',
1028
+ demand: processSelection.effortScore
1029
+ },
672
1030
  } )
673
1031
  logger.info(
674
1032
  `[executive] ◆ deliberate propose tick=${state.tick} ` +
@@ -688,11 +1046,11 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
688
1046
  state,
689
1047
  qualityModulation,
690
1048
  epistemicUncertainty,
691
- pendingMessages: [ ...this._messageQueue.pendingMessages ],
692
1049
  focus,
693
1050
  deps: promptDeps,
694
1051
  recentActionTypes: [ ...this._recentActionTypes ],
695
1052
  mode: 'master',
1053
+ activeConversations: this._activeConversations(),
696
1054
  ideationCandidates
697
1055
  } )
698
1056
 
@@ -706,7 +1064,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
706
1064
  // D2: context counts for per-tick cognitive state snapshot
707
1065
  workingMemoryItems: execContext.workingMemory.length,
708
1066
  goalCount: execContext.goals.length,
709
- pendingMessages: this._messageQueue.pendingMessages.length,
710
1067
  beliefCount: execContext.beliefs.length,
711
1068
  beliefsOmitted: execContext.beliefsOmitted,
712
1069
  promptPath: this._llmDirector?.writeDebugPrompt( state.tick, systemPrompt, userMessage ) ?? ''
@@ -724,7 +1081,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
724
1081
  // MODEL_ROUTING W0 — the effort gate already weighed this tick's demand
725
1082
  // (uncertainty, prior confidence, novelty, a pending reply, stress load);
726
1083
  // 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 }
1084
+ const masterMeta: LLMCallMeta = { category: 'executive', attribute: 'master', process: 'decision', function: '-', demand: processSelection.effortScore }
728
1085
  const result = this._chunkBroadcaster
729
1086
  ? await this._llmDirector.callStream( systemPrompt, userMessage, state.tick, this._chunkBroadcaster, undefined, masterMeta )
730
1087
  : await this._llmDirector.call( systemPrompt, userMessage, state.tick, undefined, masterMeta )
@@ -794,11 +1151,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
794
1151
  plansCount: executiveOutput.plans?.length ?? 0,
795
1152
  goalsNew: executiveOutput.newGoals ?? [],
796
1153
  goalsAbandon: executiveOutput.goalsToAbandon ?? [],
797
- replies: ( executiveOutput.conversationReplies ?? [] ).map( r => ( {
798
- targetEntityId: r.targetEntityId,
799
- targetEntityName: r.targetEntityName,
800
- messages: r.messages,
801
- } ) ),
802
1154
  hasIntrospection: !!executiveOutput.introspection,
803
1155
  hasNarrative: !!executiveOutput.narrative
804
1156
  } )
@@ -811,6 +1163,23 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
811
1163
  hasNarrative: !!executiveOutput.narrative
812
1164
  } )
813
1165
 
1166
+ // ── DeliberationCache learning (slow path) ───────────────
1167
+ // extractFingerprint is pure over the frozen `state`, so recomputing here
1168
+ // yields the identical vector used on entry — no need to thread it through.
1169
+ if( this._cache ){
1170
+ const learnTick = footprint.tickObserved as unknown as number
1171
+ if( this._pendingVerify ){
1172
+ const match = this._actionTypesMatch(
1173
+ this._pendingVerify.cachedActionTypes,
1174
+ executiveOutput.actions.map( a => a.type ),
1175
+ )
1176
+ this._cache.updateCompetence( this._pendingVerify.fingerprint, match ? 1 : 0, learnTick )
1177
+ this._pendingVerify = null
1178
+ } else {
1179
+ this._cache.learn( extractFingerprint( state ), executiveOutput, learnTick )
1180
+ }
1181
+ }
1182
+
814
1183
  return executiveOutput
815
1184
  }
816
1185
 
@@ -889,7 +1258,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
889
1258
  )
890
1259
 
891
1260
  // ── Flush pending escalation percepts ──────────────────────
892
- // Convert buffered audition.task.signal events into high-salience
1261
+ // Convert buffered executive.facet.handoff events into high-salience
893
1262
  // percept entities so Exteroception surfaces them as
894
1263
  // "## Percepts (What I Notice)" on the NEXT master cycle.
895
1264
  // The master sees them as environmental signals — not as messages to reply to.
@@ -976,14 +1345,57 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
976
1345
  } )
977
1346
 
978
1347
  // Clear processed messages
979
- this._messageQueue.clearProcessedMessages()
980
1348
 
981
- // Merge escalation percepts into final commands
1349
+ // Merge escalation percepts into the final commands.
1350
+ //
1351
+ // `_reconcileUndertakings` used to sit here, discharging promises the engine
1352
+ // judged kept and dropping restatements. Both are gone with the undertaking
1353
+ // harness: a promise is no longer a percept, so there is nothing to
1354
+ // reconcile. What the facet declared rides the sync tract to the master's own
1355
+ // prompt, and what to do about it — including whether it is still worth
1356
+ // doing — is the mind's, through the goal machinery it already has.
982
1357
  if( escalationPercepts.length ){
983
1358
  commands.set ??= []
984
1359
  commands.set.push( ...escalationPercepts )
985
1360
  }
986
1361
 
1362
+ // ── Persist deliberation cache (mirrors the rolling summary) ──
1363
+ // Written each executive cycle so the learned patterns survive snapshot/
1364
+ // restore (R2). Bounded by maxPatterns; the stored output objects are only
1365
+ // ever read back, so state's deep-freeze of them is harmless.
1366
+ if( this._cache ){
1367
+ commands.set ??= []
1368
+ commands.set.push( {
1369
+ id: 'executive-deliberation-cache',
1370
+ type: 'executive.cache',
1371
+ metadata: { snapshot: this._cache.snapshot() },
1372
+ } )
1373
+
1374
+ // Surface cache state the R2-safe way: metrics ride the returned
1375
+ // StateCommands (never a frozen-state mutation), and hit/miss events
1376
+ // publish from this committed path so faculties can react to how
1377
+ // automatic the Will is becoming.
1378
+ const total = this._cache.hitCount + this._cache.missCount
1379
+ commands.metrics ??= []
1380
+ commands.metrics.push(
1381
+ [ 'cache.hit', this._lastCacheHit ? 1 : 0 ],
1382
+ [ 'cache.confidence', this._lastCacheConfidence ],
1383
+ [ 'cache.hit_rate', total > 0 ? this._cache.hitCount / total : 0 ],
1384
+ [ 'cache.size', this._cache.size ],
1385
+ )
1386
+ this._bus?.publish( this._lastCacheHit
1387
+ ? {
1388
+ type: 'cache.hit', version: 1, sourceEngine: this.name,
1389
+ salience: this._lastCacheConfidence,
1390
+ payload: { confidence: this._lastCacheConfidence, neighborCount: this._lastCacheNeighborCount },
1391
+ }
1392
+ : {
1393
+ type: 'cache.miss', version: 1, sourceEngine: this.name,
1394
+ salience: 0.3,
1395
+ payload: { confidence: this._lastCacheConfidence },
1396
+ } )
1397
+ }
1398
+
987
1399
  // Track entities modified
988
1400
  if( commands.set?.length )
989
1401
  for( const entity of commands.set )
@@ -1001,122 +1413,288 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
1001
1413
 
1002
1414
  // ── Private helpers ────────────────────────────────────────
1003
1415
 
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
- }
1416
+ /**
1417
+ * The people the mind is in conversation with right now, newest first.
1418
+ *
1419
+ * Pruned against the supervisor's live facets on every read: a reaped facet is a
1420
+ * conversation that has ended, and a master that still believes it is mid-thread
1421
+ * with someone reasons about a table that is no longer there.
1422
+ */
1423
+ private _activeConversations(): { entityId: string; name?: string; sinceTick: number }[] {
1424
+ const live = this._facetSupervisor.liveFacetIds()
1425
+ for( const id of [ ...this._facetSubjects.keys() ] )
1426
+ if( !live.has( id ) ) this._facetSubjects.delete( id )
1427
+
1428
+ return [ ...this._facetSubjects.values() ]
1429
+ .sort( ( a, b ) => b.tick - a.tick )
1430
+ .map( s => ({
1431
+ entityId: s.entityId,
1432
+ ...( s.name ? { name: s.name } : {} ),
1433
+ sinceTick: s.tick,
1434
+ ...( s.concluded ? { concluded: s.concluded } : {} ),
1435
+ ...( s.promised?.length ? { promised: s.promised } : {} ),
1436
+ }) )
1437
+ }
1021
1438
 
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
1032
- }
1439
+ /**
1440
+ * Facet sync — remember WHO each facet is with, and wake the master.
1441
+ *
1442
+ * Reached from `onCognitiveEvent`, NOT from its own `bus.subscribe`. The bus
1443
+ * stores one subscription per engineId (`_subscriptions.set( engineId, … )`),
1444
+ * so a second `subscribe(this.name, …)` silently REPLACES the first — and the
1445
+ * orchestrator registers `subscribe( engine.name, engine.subscribes(), )`
1446
+ * after `attachBus`, which replaced everything registered here. Two dedicated
1447
+ * handlers used to be installed at this point; the second overwrote the first
1448
+ * and the orchestrator then overwrote that, so neither ever ran. The escalation
1449
+ * leg had been dead in production for its whole life: a facet could escalate,
1450
+ * the audition engine published, and nothing was listening.
1451
+ */
1452
+ /**
1453
+ * Retire undertakings the mind has already honoured, and refuse to restate one
1454
+ * it is already carrying.
1455
+ *
1456
+ * An undertaking percept says, in the first person, "I said I would reach X and
1457
+ * nothing has gone to them yet". That sentence has to stop being true at some
1458
+ * point, and nothing made it stop. Measured on a live Will: SEVEN of them
1459
+ * accumulated in state, every one still asserting nothing had been sent, while
1460
+ * a `conversation.sent` to that person sat right beside them. She read seven
1461
+ * standing unfulfilled promises every cycle and dutifully sent the same message
1462
+ * again, five times in five minutes and once more in the next session — the
1463
+ * percept meant to stop her forgetting a promise was making her unable to
1464
+ * believe she had kept it.
1465
+ *
1466
+ * Discharged by EVIDENCE, not by a timer: a `conversation.sent` to that target,
1467
+ * written no earlier than the undertaking, means the contact happened. That
1468
+ * record is durable and snapshots with the state, so the discharge survives a
1469
+ * restart exactly as the promise does — which the tick-scoped satiation in
1470
+ * `enactionFootprint` deliberately cannot.
1471
+ *
1472
+ * It stays a decision, not an erasure. Retiring the percept removes the standing
1473
+ * claim that the words are unsent; whether to say more to that person is then an
1474
+ * ordinary competition like any other.
1475
+ */
1476
+ private _onFacetSync( event: CognitiveEvent ): void {
1477
+ const payload = event.payload as {
1478
+ facetId?: string
1479
+ reasoning?: string
1480
+ confidence?: number
1481
+ tick?: number
1482
+ subjectEntityId?: string
1483
+ subjectName?: string
1484
+ }
1033
1485
 
1034
- this._gatingState.salienceBuffer.push( {
1035
- event: syntheticEvent,
1036
- tick: payload.tick ?? 0
1037
- } )
1486
+ // Remember WHO this facet is with, and WHAT it worked out there. The second
1487
+ // half is the return leg: `reasoning` has always been on this payload and was
1488
+ // read into a local and then dropped, so the master learned that its attention
1489
+ // had been engaged and never what it concluded.
1490
+ //
1491
+ // Carried whole, not clipped. For the master this is the only copy — the
1492
+ // facet's own history is in the facet, and once that conversation ends the
1493
+ // thinking is gone. Live facets are few (one per open conversation), so this
1494
+ // is bounded by how many people the mind is talking to, not by traffic.
1495
+ if( payload.facetId && payload.subjectEntityId ){
1496
+ const prior = this._facetSubjects.get( payload.facetId )
1497
+ this._facetSubjects.set( payload.facetId, {
1498
+ entityId: payload.subjectEntityId,
1499
+ ...( payload.subjectName ? { name: payload.subjectName } : {} ),
1500
+ tick: payload.tick ?? this._lastExecutiveTick ?? 0,
1501
+ ...( payload.reasoning ? { concluded: payload.reasoning } : {} ),
1502
+ // Promises ride along rather than being re-derived: a commitment the facet
1503
+ // DECLARED is a fact, while the same thing read back out of prose is a
1504
+ // guess. Preserved across syncs until the master has read them.
1505
+ ...( prior?.promised?.length ? { promised: prior.promised } : {} ),
1506
+ })
1507
+ }
1038
1508
 
1039
- logger.info(
1040
- `[executive] master received facet sync from ${payload.facetId} ` +
1041
- `(confidence=${payload.confidence?.toFixed( 2 )})`
1042
- )
1043
- }
1509
+ // Unconditional (not gated on WORKSPACE_THRESHOLD like ordinary traffic):
1510
+ // a facet reporting back is the master's own attention returning, and the
1511
+ // point of the spike is that it re-evaluates rather than waiting out its
1512
+ // interval.
1513
+ this._gatingState.salienceBuffer.push({
1514
+ event,
1515
+ tick: payload.tick ?? event.logicalTime ?? 0,
1516
+ })
1517
+
1518
+ logger.info(
1519
+ `[executive] master received facet sync from ${payload.facetId}` +
1520
+ ( payload.subjectName || payload.subjectEntityId
1521
+ ? ` (with ${payload.subjectName ?? payload.subjectEntityId})` : '') +
1522
+ ` (confidence=${payload.confidence?.toFixed( 2 )})`
1044
1523
  )
1524
+ }
1045
1525
 
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).
1526
+ /**
1527
+ * A focused part of me surfaced something the singular seat owns — work to plan
1528
+ * (`escalation`) or an intention toward a third party (`undertaking`).
1529
+ *
1530
+ * ONE handler for every facet type. This was `_onAuditionTaskSignal`, listening
1531
+ * on a topic named for one sense engine and typed with one sense engine's nouns
1532
+ * (`entityId`, `threadId`), which meant a planning, supervision or deliberation
1533
+ * facet had no way to hand anything up at all. See EscalationBuffer for the full
1534
+ * rationale; new kinds go in `HandoffBody`, not in a new topic and a new handler
1535
+ * beside this one.
1536
+ *
1537
+ * Master stays out of the reply path entirely:
1538
+ * • The facet has already said (or will say) whatever the person in front of
1539
+ * it needed to hear.
1540
+ * • The master's job is purely cognitive: create a [PLANS] block, update
1541
+ * goals, reflect, or decide whether it still means to make that contact.
1542
+ * Any follow-up communication flows through the agency competition —
1543
+ * NEVER via [REPLY].
1544
+ *
1545
+ * Buffered rather than written directly: state is read-only here, so
1546
+ * `EscalationBuffer.drainToPercepts()` emits it as a StateCommand on the next
1547
+ * master cycle, where Exteroception surfaces it under "## Percepts (What I Notice)".
1548
+ */
1549
+ private _onFacetHandoff( event: CognitiveEvent ): void {
1550
+ const payload = event.payload as {
1551
+ facetId?: string
1552
+ subjectEntityId?: string
1553
+ subjectName?: string
1554
+ threadId?: string
1555
+ confidence?: number
1556
+ tick?: number
1557
+ body: HandoffBody
1558
+ }
1559
+ if( !payload?.body?.kind ) return
1560
+
1561
+ // NOW, not the last time the master happened to run.
1051
1562
  //
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].
1563
+ // This was `this._lastExecutiveTick`, so an undertaking formed at tick 900 was
1564
+ // stamped 780 if that was the last master cycle. `_reconcileUndertakings`
1565
+ // discharges on "contacted at or after `madeAt`", which means a message sent at
1566
+ // tick 800 BEFORE the promise existed retired it. Over-discharge is the safe
1567
+ // direction, which is why it never surfaced as a symptom, but it made the promise
1568
+ // unfalsifiable: it could be marked kept by something that happened first.
1569
+ const tick = payload.tick ?? this._currentTick
1570
+
1571
+ // AN UNDERTAKING RIDES THE TRACT, NOT A HARNESS OF ITS OWN.
1059
1572
  //
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
- }
1573
+ // It used to become a percept plus a standing ideomotor intent plus a bespoke
1574
+ // discharge rule the engine deciding how hard a promise pulls and when it
1575
+ // counts as kept. That is judgement, and judgement is the mind's. What is not
1576
+ // the mind's to supply is the FACT crossing from where its attention was to
1577
+ // where its singular seat is; that crossing is anatomy.
1578
+ //
1579
+ // So it is filed with what the facet concluded and rendered to the master at
1580
+ // its next cycle. Reading that it promised someone something and has not done
1581
+ // it, the master may form a goal — which it can already do, and which the goal
1582
+ // machinery then lifts, plans and completes. Or it may not, and that is a
1583
+ // decision rather than a mechanism failing.
1584
+ //
1585
+ // The old route could not have worked anyway: a percept is swept at +2 ticks
1586
+ // and decays out of working memory at about +9, while the master's interval
1587
+ // is 15. The notice was usually gone before the seat it was addressed to ran.
1588
+ // Spike the salience buffer FIRST, for both kinds. The master must NOT be
1589
+ // handed the inbound as a message to answer — that would make it produce a
1590
+ // [REPLY], duplicating the facet's and breaking the facet/master boundary.
1591
+ // (The queue that once carried inbound this way was removed in #114: nothing
1592
+ // ever filled it.) Done before the undertaking branch returns, so a promise
1593
+ // still pulls the master's next cycle forward rather than waiting out the
1594
+ // interval — which matters more now that the promise is what it will read.
1595
+ this._gatingState.salienceBuffer.push({ event, tick })
1596
+
1597
+ const from = payload.subjectName ?? payload.subjectEntityId ?? payload.facetId ?? 'a focus'
1598
+
1599
+ if( payload.body.kind === 'undertaking'){
1600
+ const body = payload.body
1601
+ // Keyed by facet because that is what the master's view of its open threads
1602
+ // is keyed by. A handoff with no facet id has nowhere to file — say so
1603
+ // rather than dropping it quietly, which is how a promise disappears with
1604
+ // nobody able to tell whether it was ever made.
1605
+ if( !payload.facetId ){
1606
+ logger.warn(`[executive] commitment "${ body.what }" arrived with no facetId — nowhere to file it`)
1607
+ return
1608
+ }
1609
+ // Created if absent: a facet can hand something up before its first sync,
1610
+ // and requiring the entry to exist first made that promise vanish.
1611
+ const at = this._facetSubjects.get( payload.facetId ) ?? {
1612
+ entityId: payload.subjectEntityId ?? payload.facetId,
1613
+ ...( payload.subjectName ? { name: payload.subjectName } : {} ),
1614
+ tick,
1615
+ }
1616
+ at.promised = [
1617
+ // Deduped on WHAT was promised, falling back to the target when two
1618
+ // wordings mean the same contact: restating a promise is the same
1619
+ // promise, and the master does not need telling twice.
1620
+ ...( at.promised ?? [] ).filter( p =>
1621
+ p.what !== body.what && !( body.target !== undefined && p.target === body.target ) ),
1622
+ { what: body.what,
1623
+ ...( body.target ? { target: body.target } : {} ),
1624
+ ...( body.gist ? { gist: body.gist } : {} ), tick },
1625
+ ]
1626
+ this._facetSubjects.set( payload.facetId, at )
1627
+ logger.info(
1628
+ `[executive] filed commitment from ${from} → "${ body.what }" ` +
1629
+ `(it reads this at its next cycle; what to do about it is its own call)`
1630
+ )
1631
+ return
1632
+ }
1077
1633
 
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
- }
1634
+ this._escalations.push({
1635
+ tick,
1636
+ ...( payload.facetId ? { facetId: payload.facetId } : {} ),
1637
+ ...( payload.subjectEntityId ? { subjectEntityId: payload.subjectEntityId } : {} ),
1638
+ ...( payload.subjectName ? { subjectName: payload.subjectName } : {} ),
1639
+ ...( payload.threadId ? { threadId: payload.threadId } : {} ),
1640
+ body: { ...payload.body, reasoning: ( payload.body.reasoning ?? '').slice( 0, 400 ) },
1641
+ })
1094
1642
 
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
- }
1643
+ logger.info(
1644
+ `[executive] master queued escalation percept from ${from} ` +
1645
+ `(confidence=${payload.confidence?.toFixed( 2 )})`
1646
+ )
1647
+ }
1108
1648
 
1109
- this._gatingState.salienceBuffer.push( {
1110
- event: syntheticEvent,
1111
- tick: this._lastExecutiveTick ?? 0
1112
- } )
1649
+ // ── DeliberationCache wiring ───────────────────────────────
1113
1650
 
1114
- logger.info(
1115
- `[executive] master queued escalation percept from entity ${payload.entityId} ` +
1116
- `(confidence=${payload.confidence.toFixed( 2 )})`
1117
- )
1118
- }
1119
- )
1651
+ /** Enable the deliberation cache (off by default). Call during mind assembly. */
1652
+ enableCache( config?: DeliberationCacheConfig ): void {
1653
+ this._cache = new DeliberationCache( config )
1654
+ }
1655
+
1656
+ /** True when the cache is active. */
1657
+ get cacheEnabled(): boolean { return this._cache !== null }
1658
+
1659
+ /** Telemetry snapshot for harnesses / eval. Null when disabled. */
1660
+ cacheStats(): { size: number; hits: number; misses: number } | null {
1661
+ if( !this._cache ) return null
1662
+ return { size: this._cache.size, hits: this._cache.hitCount, misses: this._cache.missCount }
1663
+ }
1664
+
1665
+ /**
1666
+ * Reafference hook — update cache competence from a confirmed action outcome.
1667
+ * Optional, layered on top of the inline verify loop. Reward follows the
1668
+ * research sketch: mean of (action succeeded, stress relief, goal progress).
1669
+ */
1670
+ onActionOutcome(
1671
+ state: ReadonlySimulationState,
1672
+ tick: Tick,
1673
+ success: boolean,
1674
+ stressDelta: number,
1675
+ goalProgressDelta: number,
1676
+ ): void {
1677
+ if( !this._cache ) return
1678
+ const reward = (
1679
+ ( success ? 1 : 0 ) +
1680
+ ( 1 - Math.max( 0, Math.min( 1, stressDelta ) ) ) +
1681
+ Math.max( 0, Math.min( 1, goalProgressDelta ) )
1682
+ ) / 3
1683
+ this._cache.updateCompetence( extractFingerprint( state ), reward, tick )
1684
+ }
1685
+
1686
+ private _actionTypesMatch( a: string[], b: string[] ): boolean {
1687
+ if( a.length !== b.length ) return false
1688
+ for( let i = 0; i < a.length; i++ ) if( a[ i ] !== b[ i ] ) return false
1689
+ return true
1690
+ }
1691
+
1692
+ private _restoreDeliberationCache( state: ReadonlySimulationState ): void {
1693
+ if( !this._cache ) return
1694
+ const entity = state.entities.get('executive-deliberation-cache')
1695
+ if( !entity ) return
1696
+ const snap = entity.metadata?.[ 'snapshot' ] as DeliberationCacheSnapshot | undefined
1697
+ if( snap ) this._cache.restore( snap )
1120
1698
  }
1121
1699
 
1122
1700
  private _restoreSummarizer( state: ReadonlySimulationState ): void {