@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
@@ -53,6 +53,18 @@ export interface FacetReport {
53
53
  contextId?: string
54
54
  /** Optional dynamic instructions to append to the user message */
55
55
  instructions?: string
56
+ /**
57
+ * Attend to something else for THIS report only, leaving the facet's standing
58
+ * focus untouched.
59
+ *
60
+ * A facet's focus was a single mutable field, so anything that wanted a live
61
+ * facet to consider one different thing had to `setFocus()` first — clobbering
62
+ * whatever the facet was already set up for, and racing with any report already
63
+ * queued behind it. That is why a self-initiated message to someone the mind was
64
+ * ALREADY talking to had to be composed by a separate, transient facet that could
65
+ * not see the live conversation at all.
66
+ */
67
+ focus?: FocusSection
56
68
  }
57
69
 
58
70
  export interface FacetDecision {
@@ -63,6 +75,16 @@ export interface FacetDecision {
63
75
  decision: unknown
64
76
  reasoning: string
65
77
  confidence: number
78
+ /**
79
+ * Sim tick this decision was reasoned at.
80
+ *
81
+ * The facet has always known it and never passed it on, so a subscriber writing
82
+ * state in response had no deterministic clock and reached for a process-local
83
+ * counter instead — which resets on restart and collides. Never wall-clock: this
84
+ * reaches ids that live in state, and a wall-clock id makes recorded and replayed
85
+ * runs diverge (R2).
86
+ */
87
+ tick: number
66
88
  }
67
89
 
68
90
  export type FacetEventListener = ( decision: FacetDecision ) => void
@@ -134,6 +156,19 @@ export class ExecutiveFacet {
134
156
  private _facetReasoningHistory: string[] = []
135
157
  private _masterSyncHistory: string[] = []
136
158
 
159
+ /**
160
+ * This facet's own prior reasoning, for the supervisor to carry to whichever
161
+ * facet takes over the same keyed thread. Continuity belongs to the thread, not
162
+ * to the instance holding it: a facet reaped mid-conversation used to take the
163
+ * mind's private thinking about that person with it.
164
+ */
165
+ get reasoningHistory(): string[] { return [ ...this._facetReasoningHistory ] }
166
+
167
+ /** Resume a thread's reasoning on spawn (supervisor-only; see FacetSpawnDeps.key). */
168
+ restoreReasoningHistory( history: string[] ): void {
169
+ this._facetReasoningHistory = [ ...history ]
170
+ }
171
+
137
172
  /** Confidence of this facet's *previous* decision — a dual-process gate signal. */
138
173
  private _lastConfidence = 0.5
139
174
 
@@ -260,9 +295,7 @@ export class ExecutiveFacet {
260
295
  private _launchReason( report: FacetReport ): void {
261
296
  this._inflight++
262
297
  this._reason( report )
263
- .catch( err =>
264
- logger.error(`[executive.facet] ${this.facetId} reasoning error:`, err )
265
- )
298
+ .catch( err => logger.error(`[executive.facet] ${this.facetId} reasoning error:`, err ) )
266
299
  .finally( () => {
267
300
  this._inflight--
268
301
  this.markActive( ( this._currentStateRef?.tick as number ) ?? this._lastActiveTick )
@@ -322,9 +355,14 @@ export class ExecutiveFacet {
322
355
 
323
356
  logger.info(`[executive.facet] ${this.facetId} synced from master (tick=${payload.tick})`)
324
357
 
325
- // Store master's latest reasoning for context in next report()
358
+ // Store the wider reasoning for context in the next report().
359
+ //
360
+ // Rendered in the FIRST PERSON and with no mention of a "master": this is the
361
+ // same mind's thinking arriving from where the rest of its attention has been,
362
+ // not a report from a superior. Naming it "Master sync" gave the facet a second
363
+ // party to address — and it addressed it, out loud, on the outbound channel.
326
364
  if( payload.reasoning )
327
- this._masterSyncHistory.push(`[Master sync — tick ${payload.tick}] ${payload.reasoning.slice( 0, 400 )}`)
365
+ this._masterSyncHistory.push(`[tick ${payload.tick}] ${payload.reasoning.slice( 0, 400 )}`)
328
366
 
329
367
  // Keep only last 5 sync entries
330
368
  if( this._masterSyncHistory.length > 5 )
@@ -338,7 +376,11 @@ export class ExecutiveFacet {
338
376
  if( !this._currentStateRef )
339
377
  throw new Error(`[executive.facet] ${this.facetId} no state reference available`)
340
378
 
341
- if( !this._currentFocus )
379
+ // This report's focus: its own if it carried one, else the facet's standing
380
+ // one. A per-report focus never mutates `_currentFocus`, so a live thread can
381
+ // be asked to attend to one different thing and come straight back.
382
+ const reportFocus = report.focus ?? this._currentFocus
383
+ if( !reportFocus )
342
384
  throw new Error(`[executive.facet] ${this.facetId} no focus set. Call setFocus() before report().`)
343
385
 
344
386
  const currentState = this._currentStateRef
@@ -346,7 +388,7 @@ export class ExecutiveFacet {
346
388
  // Build fresh context from current state
347
389
  // A focus may supply a recall query (e.g. the live conversation message) to
348
390
  // drive the single "## Relevant Memories" section — one recall surface (§5).
349
- const execContext = await PromptFactory.buildFreshContext( this._contextDeps, currentState, this._currentFocus?.recallQuery )
391
+ const execContext = await PromptFactory.buildFreshContext( this._contextDeps, currentState, reportFocus.recallQuery )
350
392
 
351
393
  const qualityModulation = PromptFactory.computeQualityModulation( currentState )
352
394
  const epistemicUncertainty = PromptFactory.computeEpistemicUncertainty( execContext, currentState )
@@ -355,10 +397,10 @@ export class ExecutiveFacet {
355
397
  // the appropriate content via setFocus()
356
398
  const focus: FocusSection = this._masterSyncHistory.length > 0
357
399
  ? {
358
- ...this._currentFocus,
359
- content: `${this._currentFocus.content}\n\n## Master Consciousness Updates\n${this._masterSyncHistory.join('\n')}`
400
+ ...reportFocus,
401
+ content: `${reportFocus.content}\n\n## What I've Been Turning Over\n${this._masterSyncHistory.join('\n')}`
360
402
  }
361
- : this._currentFocus
403
+ : reportFocus
362
404
 
363
405
  // Build system prompt using PromptFactory — same schema as master, [REPLY] gated out.
364
406
  const systemPrompt = PromptFactory.buildSystemPrompt( {
@@ -377,7 +419,7 @@ export class ExecutiveFacet {
377
419
  // isn't cold on each cycle. Injected before the caller's instructions so the
378
420
  // caller content always comes last (highest recency bias from the LLM).
379
421
  const continuityBlock = this._facetReasoningHistory.length > 0
380
- ? `## My Prior Reasoning (this facet)\n${this._facetReasoningHistory.join('\n')}`
422
+ ? `## Where My Thinking Had Got To\n${this._facetReasoningHistory.join('\n')}`
381
423
  : ''
382
424
 
383
425
  const reportContent = [
@@ -399,7 +441,7 @@ export class ExecutiveFacet {
399
441
  stressLoad: currentState.metrics.get('stress.load') ?? 0,
400
442
  // A facet's "stakes-bearing moment" is a live message awaiting reply (conversation
401
443
  // facets set focus.recallQuery to it); planning/other facets rely on uncertainty.
402
- hasPendingMessage: !!this._currentFocus?.recallQuery,
444
+ hasPendingMessage: !!reportFocus.recallQuery,
403
445
  }, deliberateThreshold )
404
446
 
405
447
  let ideationCandidates: IdeationCandidate[] | undefined
@@ -410,7 +452,6 @@ export class ExecutiveFacet {
410
452
  state: currentState,
411
453
  qualityModulation,
412
454
  epistemicUncertainty,
413
- pendingMessages: [],
414
455
  focus,
415
456
  deps: this._promptDeps,
416
457
  recentActionTypes: [],
@@ -424,7 +465,14 @@ export class ExecutiveFacet {
424
465
  ideationUserMessage,
425
466
  tick: currentState.tick,
426
467
  proposeTemperature,
427
- meta: { category: 'executive', attribute: 'facet', function: this._currentFocus?.function ?? 'ideation', scope: this.facetId, demand: processSelection.effortScore },
468
+ meta: {
469
+ category: 'executive',
470
+ attribute: 'facet',
471
+ process: 'ideation',
472
+ function: reportFocus.function ?? '-',
473
+ scope: this.facetId,
474
+ demand: processSelection.effortScore
475
+ },
428
476
  } )
429
477
  logger.info(
430
478
  `[executive.facet] ${this.facetId} ◆ deliberate propose tick=${currentState.tick} ` +
@@ -437,7 +485,6 @@ export class ExecutiveFacet {
437
485
  state: currentState,
438
486
  qualityModulation,
439
487
  epistemicUncertainty,
440
- pendingMessages: [],
441
488
  focus,
442
489
  deps: this._promptDeps,
443
490
  recentActionTypes: [],
@@ -471,11 +518,12 @@ export class ExecutiveFacet {
471
518
  const facetMeta: LLMCallMeta = {
472
519
  category: 'executive',
473
520
  attribute: 'facet',
521
+ process: 'decision',
474
522
  // A focus that declares no function is making its decision call, the
475
523
  // facet's analogue of master's 'decision'. This previously fell back to
476
524
  // 'facet' — an *attribute* value, which quietly created a bogus bucket
477
525
  // in the by-function cost breakdown. The typed axes caught it.
478
- function: this._currentFocus?.function ?? 'decision',
526
+ function: reportFocus.function ?? '-',
479
527
  scope: this.facetId,
480
528
  demand: processSelection.effortScore,
481
529
  }
@@ -555,7 +603,8 @@ export class ExecutiveFacet {
555
603
  respondingToType: report.type,
556
604
  decision: this._extractDecisionPayload( output, focus ),
557
605
  reasoning: output.reasoning,
558
- confidence: output.confidence
606
+ confidence: output.confidence,
607
+ tick: currentState.tick as unknown as number
559
608
  }
560
609
 
561
610
  // Promote subscriber-visible fields from the decision object to the event payload
@@ -566,7 +615,7 @@ export class ExecutiveFacet {
566
615
  ? decision.decision as Record<string, unknown>
567
616
  : {}
568
617
 
569
- this._bus.publish( {
618
+ this._bus.publish({
570
619
  type: 'executive.facet.progress',
571
620
  version: 1,
572
621
  sourceEngine: `executive-facet-${this.facetId}`,
@@ -587,7 +636,7 @@ export class ExecutiveFacet {
587
636
  contextId: report.contextId,
588
637
  tick: currentState.tick
589
638
  }
590
- } )
639
+ })
591
640
 
592
641
  // Conscious learning about others — route name/feeling to known.entity.tracker (the
593
642
  // dossier owner) the same way the master does, so routine (un-escalated) conversation
@@ -595,15 +644,17 @@ export class ExecutiveFacet {
595
644
  const keUpdates = dec[ 'knownEntityUpdates' ] as ExecutiveOutputFull['knownEntityUpdates'] | undefined
596
645
  if( keUpdates )
597
646
  for( const u of keUpdates )
598
- if( u.keid && u.keid !== 'agent-self' && ( u.name || u.feeling != null ) )
599
- this._bus.publish( {
600
- type: 'known.entity.learned', version: 1,
647
+ if( u.keid && u.keid !== 'agent-self' && ( u.name || u.feeling != null || u.sameAs ) )
648
+ this._bus.publish({
649
+ type: 'known.entity.learned',
650
+ version: 1,
601
651
  sourceEngine: `executive-facet-${this.facetId}`,
602
- salience: 0.5, payload: { keid: u.keid, name: u.name, feeling: u.feeling }
603
- } )
652
+ salience: u.sameAs ? 0.7 : 0.5,
653
+ payload: { keid: u.keid, name: u.name, feeling: u.feeling, sameAs: u.sameAs }
654
+ })
604
655
 
605
656
  // Sync back to master
606
- this._bus.publish( {
657
+ this._bus.publish({
607
658
  type: 'executive.facet.sync',
608
659
  version: 1,
609
660
  sourceEngine: `executive-facet-${this.facetId}`,
@@ -613,9 +664,13 @@ export class ExecutiveFacet {
613
664
  reasoning: output.reasoning,
614
665
  confidence: output.confidence,
615
666
  actionTypes: output.actions.map( a => a.type ),
667
+ // WHO this facet is engaged with (FocusSection.subject*). The master keeps
668
+ // the singular seat, so it needs the person, not just the facet number.
669
+ ...( focus.subjectEntityId ? { subjectEntityId: focus.subjectEntityId } : {} ),
670
+ ...( focus.subjectName ? { subjectName: focus.subjectName } : {} ),
616
671
  tick: currentState.tick
617
672
  }
618
- } )
673
+ })
619
674
 
620
675
  // Notify all listeners (PlanningEngine, AuditionEngine, etc.).
621
676
  //
@@ -2,7 +2,6 @@
2
2
  // src/cognition/faculties/executive.engine/gating.ts
3
3
  // ─────────────────────────────────────────────────────────────
4
4
 
5
- import type { PendingMessage } from '#faculties/executive.engine/types'
6
5
  import type { Tick, ReadonlySimulationState } from '#core/types'
7
6
  import type { GenerativeModel } from '#cognition/generative.model'
8
7
  import {
@@ -11,23 +10,28 @@ import {
11
10
  } from '#faculties/executive.engine/config'
12
11
  import type { CognitiveEvent } from '#cognition/bus'
13
12
 
14
- function hasPendingInstructions( state: ReadonlySimulationState, pendingMessages: PendingMessage[] ): boolean {
15
- for( const entity of state.entities.values() ){
13
+ /**
14
+ * Something in the world is loud enough to wake a resting mind.
15
+ *
16
+ * Percept salience is the whole test now. Two other wake sources used to sit here
17
+ * and neither could ever fire: an unprocessed `communication` entity (a type
18
+ * nothing has ever written) and a non-empty pending-message queue (never pushed
19
+ * to, and the current design forbids it — inbound reaches a facet, and the master
20
+ * learns of it through `executive.facet.handoff` → a high-salience percept, which
21
+ * this catches). Both removed with the queue itself (#114).
22
+ */
23
+ function hasPendingInstructions( state: ReadonlySimulationState ): boolean {
24
+ for( const entity of state.entities.values() )
16
25
  if( entity.type === 'percept' || entity.type === 'percept.social'){
17
26
  const salience = (entity.metadata?.salience as number) ?? 0
18
27
  if( salience > 0.7 ) return true
19
28
  }
20
29
 
21
- if( entity.type === 'communication' && !entity.metadata?.processedByExecutive )
22
- return true
23
- }
24
-
25
- return pendingMessages.length > 0
30
+ return false
26
31
  }
27
32
 
28
33
  export interface GatingDependencies {
29
34
  generativeModel: GenerativeModel
30
- pendingMessages: PendingMessage[]
31
35
  hasPendingWork: boolean
32
36
  }
33
37
 
@@ -110,7 +114,7 @@ export function evaluateGating(
110
114
  if( isResting > 0 || isSleeping > 0 ){
111
115
  const
112
116
  significantEvent = novelty > 0.8,
113
- hasPending = hasPendingInstructions( state, deps.pendingMessages )
117
+ hasPending = hasPendingInstructions( state )
114
118
 
115
119
  if( significantEvent || hasPending )
116
120
  return {
@@ -142,10 +146,6 @@ export function evaluateGating(
142
146
  if( energy < 15 )
143
147
  return { shouldActivate: true, reason: 'energy_critical', cleanedBuffer }
144
148
 
145
- // ── Pending messages — always fire ────────────────────────
146
- if( deps.pendingMessages.length > 0 )
147
- return { shouldActivate: true, reason: 'pending_message', cleanedBuffer }
148
-
149
149
  // ── Normal interval scheduling ────────────────────────────
150
150
  if( tick - gs.lastExecutiveTick >= gs.executiveInterval ){
151
151
  // Prediction-error gating — uses GenerativeModel.observe() which returns { gated }
@@ -3,7 +3,7 @@
3
3
  // ─────────────────────────────────────────────────────────────
4
4
 
5
5
  import { logger } from '#core/logger'
6
- import { REPLY_TEXT_TAG } from '#llm/wire.contracts'
6
+ import { REPLY_TEXT_TAG, NO_MESSAGE_TAG, NO_MESSAGE_OPEN, stripProtocolMarkers } from '#llm/wire.contracts'
7
7
  import type { ReadonlySimulationState } from '#core/types'
8
8
  import type { ExecutiveOutputFull, ExecutiveOutputMinimal, IdeationCandidate, IdeationOutput } from '#faculties/executive.engine/types'
9
9
 
@@ -27,7 +27,9 @@ export function parseResponse(
27
27
 
28
28
  let
29
29
  actions: Array<{ type: string; reasoning: string; expectedOutcome: string }>,
30
- confidence = 0.5
30
+ confidence = 0.5,
31
+ /** The mind's own sentence, when the response parsed as JSON and carried one. */
32
+ stated: string | undefined
31
33
 
32
34
  // Strategy 1: direct JSON.parse
33
35
  try {
@@ -41,6 +43,7 @@ export function parseResponse(
41
43
 
42
44
  actions = parsed.actions
43
45
  confidence = parsed.confidence ?? 0.5
46
+ stated = typeof parsed.reasoning === 'string' ? parsed.reasoning : undefined
44
47
  }
45
48
  catch {
46
49
  // Strategy 2: balanced-bracket extractor
@@ -60,14 +63,45 @@ export function parseResponse(
60
63
  confidence = confidenceMatch ? parseFloat( confidenceMatch[1]! ) : 0.5
61
64
  }
62
65
 
63
- // Parse tagged blocks from the extracted JSON text
66
+ // Tagged blocks are scanned over the FULL text — they are documented as embedded
67
+ // in the reasoning field, but a model puts them wherever it likes and the scan
68
+ // has always been forgiving about that. Keep that surface.
64
69
  const full = parseTaggedBlocks({ actions, reasoning: fullText, confidence }, state )
65
70
 
71
+ // …but `reasoning` itself is the mind's SENTENCE, not its serialization.
72
+ //
73
+ // It was `fullText` — the whole JSON blob — for every consumer downstream, and
74
+ // that was invisible for as long as nothing read it as prose. Three things do:
75
+ // the session log records it, a facet is handed the master's as "What I've Been
76
+ // Turning Over" (clipped to 400 chars, so facets have been reading the master's
77
+ // JSON truncated mid-object), and the master now reads a facet's back as what it
78
+ // worked out. Seen live in a prompt: `What I worked out there: { "actions": [...
79
+ //
80
+ // The scan surface and the sentence were the same string only because nothing
81
+ // had ever needed them to be different.
82
+ // Stripped of the tagged blocks, because the format tells the mind to embed them
83
+ // IN this field — so the sentence and the structured payload arrive in one
84
+ // string, and handing the whole thing back as "what I worked out" would just
85
+ // move the serialization problem one level in.
86
+ if( stated ) full.reasoning = stripTaggedBlocks( stated )
87
+
66
88
  // [REPLY_TEXT] is a plain-text block that lives OUTSIDE the JSON code block.
67
89
  // Search the full response text so we find it even when the LLM used a code fence.
68
90
  const replyText = extractTextBlock( responseText, REPLY_TEXT_TAG )
69
91
  if( replyText ) full.replyText = replyText
70
92
 
93
+ // A declared decision not to speak. Its own field rather than an empty
94
+ // replyText, because "I chose silence" and "the facet produced nothing" are
95
+ // different events: one is a decision worth recording, the other a failure worth
96
+ // noticing, and collapsing them hides both.
97
+ // Presence of the MARKER is the decision — not the content, which may legitimately
98
+ // be empty. `extractTextBlock` returns null for an absent tag AND for a present
99
+ // but empty one, so it cannot tell them apart; testing it for undefined (as this
100
+ // first did) is true in every case and made the mind mute on every path. The
101
+ // integration suite caught it; a live boot would have caught it as total silence.
102
+ if( responseText.includes( NO_MESSAGE_OPEN ) )
103
+ full.noMessage = extractTextBlock( responseText, NO_MESSAGE_TAG ) ?? '(no reason given)'
104
+
71
105
  return full
72
106
  }
73
107
 
@@ -164,6 +198,29 @@ function extractBalancedArray( text: string, key: string ): string | null {
164
198
  return null
165
199
  }
166
200
 
201
+ /** Every block name the reasoning field may carry, for stripping it back to prose. */
202
+ const TAGGED_BLOCK_NAMES = [
203
+ 'PLANS', 'BELIEFS', 'INTROSPECTION', 'NARRATIVE', 'IDENTITY', 'KNOWN_ENTITIES',
204
+ 'GOALS_NEW', 'GOALS_ABANDON', 'GOALS_REPRIORITIZE', 'EFFECTORS', 'SELF_OBS',
205
+ 'SKILLS', 'REPLY_TEXT', 'ACK',
206
+ ] as const
207
+
208
+ /**
209
+ * The sentence, with the structured payload taken back out of it.
210
+ *
211
+ * Unclosed blocks are cut to the end of the string on purpose: a truncated
212
+ * `[PLANS]{…` is not prose either, and leaving it in puts half a JSON object in
213
+ * front of whoever reads this as the mind's thinking.
214
+ */
215
+ export function stripTaggedBlocks( reasoning: string ): string {
216
+ let out = reasoning
217
+ for( const tag of TAGGED_BLOCK_NAMES )
218
+ out = out
219
+ .replace( new RegExp( `\\[${ tag }\\][\\s\\S]*?\\[/${ tag }\\]`, 'g'), '')
220
+ .replace( new RegExp( `\\[${ tag }\\][\\s\\S]*$`, 'g'), '')
221
+ return out.replace( /\n{3,}/g, '\n\n').trim()
222
+ }
223
+
167
224
  /**
168
225
  * Parse tagged JSON blocks from the reasoning text.
169
226
  * The LLM embeds optional outputs as [TAG]...[/TAG] blocks.
@@ -193,6 +250,7 @@ function parseTaggedBlocks(
193
250
  'GOALS_REPRIORITIZE',
194
251
  'EFFECTORS',
195
252
  'SELF_OBS',
253
+ 'SKILLS',
196
254
  ],
197
255
  found = taggedTypes.filter( t => text.includes(`[${t}]`) )
198
256
  if( found.length > 0 ){
@@ -299,6 +357,13 @@ function parseTaggedBlocks(
299
357
  }
300
358
  catch { /* ignore */ }
301
359
 
360
+ // Named compound skills — the creation seam for learned composites (#114).
361
+ try {
362
+ const skillsData = parseJsonBlock('SKILLS') as { newSkills?: ExecutiveOutputFull['newSkills'] } | null
363
+ if( skillsData?.newSkills ) full.newSkills = skillsData.newSkills
364
+ }
365
+ catch { /* ignore */ }
366
+
302
367
  // [ACK] and legacy [REPLY] blocks are no longer emitted by any engine.
303
368
  // [ACK] — removed with the legacy master communication path (Phase 13.8).
304
369
  // [REPLY] — replaced by [REPLY_TEXT] plain-text block in conversation facets.
@@ -319,7 +384,15 @@ function extractBlock( text: string, tag: string ): string | null {
319
384
  * Used for [REPLY_TEXT] — content is streamed directly to the client,
320
385
  * so it must be clean prose, not a JSON payload.
321
386
  */
322
- function extractTextBlock( text: string, tag: string ): string | null {
387
+ /**
388
+ * Pull one plain-text block out of a response.
389
+ *
390
+ * Exported for the guard: this slice is where a stray marker survives into
391
+ * content, and the seam is the thing worth testing. `parseResponse` needs a full
392
+ * well-formed response around it, which is exactly the setup that hides a defect
393
+ * living in one line of string handling.
394
+ */
395
+ export function extractTextBlock( text: string, tag: string ): string | null {
323
396
  const open = `[${tag}]`
324
397
  const close = `[/${tag}]`
325
398
  const openIdx = text.indexOf( open )
@@ -330,51 +403,60 @@ function extractTextBlock( text: string, tag: string ): string | null {
330
403
  ? text.slice( contentStart, closeIdx )
331
404
  : text.slice( contentStart ) // unclosed block — take everything after the open marker
332
405
 
333
- return content.trim() || null
406
+ // A stray marker INSIDE the body survives this slice — the block runs from the
407
+ // first open to the first close after it, and anything between is content by
408
+ // construction. Live, a reply went out as four substantive bubbles and a fifth
409
+ // reading exactly `[REPLY_TEXT]`: the person received a message whose whole
410
+ // content was the name of the slot it should have filled.
411
+ return stripProtocolMarkers( content ) || null
334
412
  }
335
413
 
336
414
  /**
337
- * Build a heuristic fallback output when LLM response parsing fails.
338
- * Chooses actions based on current physiological state.
415
+ * What the executive intends when its own reasoning did not come back.
416
+ *
417
+ * Two things had to change here. Every action this produced was PROSE, not a
418
+ * schema: `observe`, `replenish energy`, `enter deep rest to reduce fatigue`,
419
+ * `calm my mind and reduce tension`, `explore`, `learn`, `express_emotion`.
420
+ * Only `rest` names anything the body can enact. The rest reach the agency, fail
421
+ * to resolve, and become an ideomotor push toward an act that does not exist —
422
+ * observed live as `master decided [observe] conf=0.4` four times across two
423
+ * boots, each one a decision that could go nowhere.
424
+ *
425
+ * And the "no urgent need" branch was INVENTION. The executive could not read
426
+ * its own thought, so it made one up and pushed it into the field as intention.
427
+ * That is the mind being told what it wants. The substrate does not need the
428
+ * help: the affordance field is always there, System 1 selects from it every
429
+ * tick without an executive, and it now carries real satiation — the monotony
430
+ * this used to break by cycling a hardcoded list is handled where monotony
431
+ * actually lives.
432
+ *
433
+ * So: reflexes stay, invention goes. Critically low energy, high sleep pressure
434
+ * or high stress still push a real regulatory stance, because a body in trouble
435
+ * should not depend on an LLM parsing correctly. Anything else yields NO action
436
+ * and lets the substrate decide, which is what it is for.
339
437
  */
340
438
  export function buildFallbackOutput(
341
439
  state: ReadonlySimulationState,
342
- recentActionTypes: string[]
440
+ _recentActionTypes: string[]
343
441
  ): ExecutiveOutputFull {
344
- const energy = state.metrics.get('energy.level') ?? 100
442
+ const energy = state.metrics.get('energy.level') ?? 100
345
443
  const sleepPressure = state.metrics.get('sleep.pressure') ?? 0
346
- const stressLoad = state.metrics.get('stress.load') ?? 0
347
-
348
- let actionType = 'observe'
349
- let actionReason = 'No urgent needs — observing surroundings.'
444
+ const stressLoad = state.metrics.get('stress.load') ?? 0
350
445
 
351
- if( energy < 20 ){
352
- actionType = 'replenish energy'
353
- actionReason = 'Energy critically low — attempting to rest.'
354
- }
355
- else if( sleepPressure > 60 ){
356
- actionType = 'enter deep rest to reduce fatigue'
357
- actionReason = 'Sleep pressure high — attempting to sleep.'
358
- }
359
- else if( stressLoad > 70 ){
360
- actionType = 'calm my mind and reduce tension'
361
- actionReason = 'Stress elevated — attempting to meditate.'
362
- }
363
- else {
364
- // Vary the fallback when low-effort actions have dominated recently
365
- const monotonous = recentActionTypes.filter( t => t === 'reflect' || t === 'observe').length >= 3
366
- if( monotonous) {
367
- const alternatives = ['explore', 'learn', 'express_emotion', 'rest']
368
- const tick = state.tick as number
369
-
370
- actionType = alternatives[ tick % alternatives.length ]!
371
- actionReason = 'Breaking monotony — choosing a varied action during LLM fallback.'
372
- }
373
- }
446
+ // Innate stances only — what the body can actually enact.
447
+ const reflex: { type: string; reasoning: string } | null =
448
+ energy < 20 ? { type: 'rest', reasoning: 'Energy critically low — I let myself recover.' }
449
+ : sleepPressure > 60 ? { type: 'rest', reasoning: 'Sleep pressure high — I let myself recover.' }
450
+ : stressLoad > 70 ? { type: 'withdraw', reasoning: 'Stress elevated — I pull back from the press of things.' }
451
+ : null
374
452
 
375
453
  return {
376
- actions: [{ type: actionType, reasoning: actionReason, expectedOutcome: 'State improves' }],
377
- reasoning: `Heuristic fallback LLM unavailable. ${actionReason}`,
454
+ actions: reflex
455
+ ? [ { type: reflex.type, reasoning: reflex.reasoning, expectedOutcome: 'The body settles' } ]
456
+ : [],
457
+ reasoning: reflex
458
+ ? `Heuristic fallback — my reasoning did not come back. ${ reflex.reasoning }`
459
+ : 'Heuristic fallback — my reasoning did not come back, and nothing about my state is pressing. I intend nothing in particular; my body goes on choosing.',
378
460
  confidence: 0.4
379
461
  }
380
462
  }