@mindot/will 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +87 -22
  2. package/dist/channels/discord.d.ts +67 -6
  3. package/dist/channels/discord.js +112 -6
  4. package/dist/channels/discord.js.map +1 -1
  5. package/dist/channels/whatsapp.d.ts +1 -1
  6. package/dist/channels/whatsapp.js +4 -1
  7. package/dist/channels/whatsapp.js.map +1 -1
  8. package/dist/cli.js +13733 -11057
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +3729 -1199
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/effectors.d.ts +1 -1
  14. package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
  15. package/package.json +1 -1
  16. package/src/channels/discord.ts +189 -11
  17. package/src/channels/types.ts +90 -0
  18. package/src/channels/whatsapp.ts +13 -4
  19. package/src/cli.ts +9 -4
  20. package/src/cognition/agency/consequence.ts +122 -1
  21. package/src/cognition/agency/conversation.aim.ts +260 -0
  22. package/src/cognition/agency/engines/action.selector.ts +85 -3
  23. package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
  24. package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
  25. package/src/cognition/agency/engines/reafference.engine.ts +129 -2
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +16 -2
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/repertoire.ts +12 -5
  30. package/src/cognition/agency/selection.scoring.ts +33 -0
  31. package/src/cognition/agency/types.ts +35 -0
  32. package/src/cognition/cache/composition.ts +232 -0
  33. package/src/cognition/cache/deliberation.cache.ts +219 -0
  34. package/src/cognition/cache/fingerprint.ts +120 -0
  35. package/src/cognition/cache/types.ts +105 -0
  36. package/src/cognition/config.mirror.entities.ts +109 -1
  37. package/src/cognition/event.schemas.ts +22 -0
  38. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  39. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  40. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  41. package/src/cognition/faculties/executive.engine/context.ts +67 -13
  42. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  43. package/src/cognition/faculties/executive.engine/engine.ts +686 -187
  44. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
  45. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
  46. package/src/cognition/faculties/executive.engine/facet.ts +89 -26
  47. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  48. package/src/cognition/faculties/executive.engine/parser.ts +21 -1
  49. package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
  50. package/src/cognition/faculties/executive.engine/types.ts +69 -0
  51. package/src/cognition/faculties/goal.manager.ts +94 -14
  52. package/src/cognition/faculties/known.entity.tracker.ts +267 -28
  53. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  54. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  55. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  56. package/src/cognition/faculties/self.model.updater.ts +19 -12
  57. package/src/cognition/faculties/social.perception.ts +47 -3
  58. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  59. package/src/cognition/faculties/working.memory.ts +10 -20
  60. package/src/cognition/identity.entity.ts +205 -0
  61. package/src/cognition/index.ts +11 -0
  62. package/src/cognition/memory/vector.adapter.ts +12 -3
  63. package/src/cognition/memory/vector.embedder.ts +54 -7
  64. package/src/cognition/persona.prior.ts +6 -0
  65. package/src/cognition/senses/audition.engine/engine.ts +404 -46
  66. package/src/cognition/senses/base.sense.engine.ts +1 -1
  67. package/src/cognition/senses/index.ts +12 -0
  68. package/src/cognition/social.identity.ts +273 -0
  69. package/src/cognition/utilities/token.tracker.ts +246 -98
  70. package/src/core/orchestrator.ts +38 -0
  71. package/src/host/boot.ts +78 -22
  72. package/src/index.ts +35 -0
  73. package/src/llm/index.ts +415 -97
  74. package/src/llm/routing.ts +204 -0
  75. package/src/llm/summarizer.ts +5 -1
  76. package/src/llm/wire.contracts.ts +19 -0
  77. package/src/pma/index.ts +67 -53
  78. package/src/runners/thin-shim.runner.ts +18 -6
  79. package/src/sdk/will.ts +121 -22
  80. package/src/stem/assembly.audit.ts +1 -0
  81. package/src/stem/guards/identity.coherence.ts +17 -6
  82. package/src/stem/index.ts +82 -5
  83. package/src/stem/mind.ts +327 -79
  84. package/src/stem/policy/arbiter.ts +49 -14
  85. package/src/stem/policy/rule.table.ts +2 -2
  86. package/src/stem/tracts/effector.controller.ts +56 -9
  87. package/src/stem/tracts/outbox.writer.ts +40 -2
  88. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
@@ -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 },
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: [],
@@ -465,11 +512,20 @@ export class ExecutiveFacet {
465
512
  // Use streaming call when a per-facet chunk handler is registered —
466
513
  // this enables entity-scoped token delivery (e.g. AuditionEngine SSE).
467
514
  // Falls back to regular call when no handler is present.
515
+ // MODEL_ROUTING W0 — same effort gate as master, so a facet's demand is
516
+ // measured on the identical scale (a live message awaiting reply is this
517
+ // focus's stakes-bearing moment, and already weighs into the score).
468
518
  const facetMeta: LLMCallMeta = {
469
519
  category: 'executive',
470
520
  attribute: 'facet',
471
- function: this._currentFocus?.function ?? 'facet',
521
+ process: 'decision',
522
+ // A focus that declares no function is making its decision call, the
523
+ // facet's analogue of master's 'decision'. This previously fell back to
524
+ // 'facet' — an *attribute* value, which quietly created a bogus bucket
525
+ // in the by-function cost breakdown. The typed axes caught it.
526
+ function: reportFocus.function ?? '-',
472
527
  scope: this.facetId,
528
+ demand: processSelection.effortScore,
473
529
  }
474
530
  const result = this._chunkHandler
475
531
  ? await this._llmDirector.callStream( systemPrompt, userMessage, currentState.tick, this._chunkHandler, undefined, facetMeta )
@@ -547,7 +603,8 @@ export class ExecutiveFacet {
547
603
  respondingToType: report.type,
548
604
  decision: this._extractDecisionPayload( output, focus ),
549
605
  reasoning: output.reasoning,
550
- confidence: output.confidence
606
+ confidence: output.confidence,
607
+ tick: currentState.tick as unknown as number
551
608
  }
552
609
 
553
610
  // Promote subscriber-visible fields from the decision object to the event payload
@@ -558,7 +615,7 @@ export class ExecutiveFacet {
558
615
  ? decision.decision as Record<string, unknown>
559
616
  : {}
560
617
 
561
- this._bus.publish( {
618
+ this._bus.publish({
562
619
  type: 'executive.facet.progress',
563
620
  version: 1,
564
621
  sourceEngine: `executive-facet-${this.facetId}`,
@@ -579,7 +636,7 @@ export class ExecutiveFacet {
579
636
  contextId: report.contextId,
580
637
  tick: currentState.tick
581
638
  }
582
- } )
639
+ })
583
640
 
584
641
  // Conscious learning about others — route name/feeling to known.entity.tracker (the
585
642
  // dossier owner) the same way the master does, so routine (un-escalated) conversation
@@ -587,15 +644,17 @@ export class ExecutiveFacet {
587
644
  const keUpdates = dec[ 'knownEntityUpdates' ] as ExecutiveOutputFull['knownEntityUpdates'] | undefined
588
645
  if( keUpdates )
589
646
  for( const u of keUpdates )
590
- if( u.keid && u.keid !== 'agent-self' && ( u.name || u.feeling != null ) )
591
- this._bus.publish( {
592
- 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,
593
651
  sourceEngine: `executive-facet-${this.facetId}`,
594
- salience: 0.5, payload: { keid: u.keid, name: u.name, feeling: u.feeling }
595
- } )
652
+ salience: u.sameAs ? 0.7 : 0.5,
653
+ payload: { keid: u.keid, name: u.name, feeling: u.feeling, sameAs: u.sameAs }
654
+ })
596
655
 
597
656
  // Sync back to master
598
- this._bus.publish( {
657
+ this._bus.publish({
599
658
  type: 'executive.facet.sync',
600
659
  version: 1,
601
660
  sourceEngine: `executive-facet-${this.facetId}`,
@@ -605,9 +664,13 @@ export class ExecutiveFacet {
605
664
  reasoning: output.reasoning,
606
665
  confidence: output.confidence,
607
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 } : {} ),
608
671
  tick: currentState.tick
609
672
  }
610
- } )
673
+ })
611
674
 
612
675
  // Notify all listeners (PlanningEngine, AuditionEngine, etc.).
613
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 } 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
 
@@ -68,6 +68,18 @@ export function parseResponse(
68
68
  const replyText = extractTextBlock( responseText, REPLY_TEXT_TAG )
69
69
  if( replyText ) full.replyText = replyText
70
70
 
71
+ // A declared decision not to speak. Its own field rather than an empty
72
+ // replyText, because "I chose silence" and "the facet produced nothing" are
73
+ // different events: one is a decision worth recording, the other a failure worth
74
+ // noticing, and collapsing them hides both.
75
+ // Presence of the MARKER is the decision — not the content, which may legitimately
76
+ // be empty. `extractTextBlock` returns null for an absent tag AND for a present
77
+ // but empty one, so it cannot tell them apart; testing it for undefined (as this
78
+ // first did) is true in every case and made the mind mute on every path. The
79
+ // integration suite caught it; a live boot would have caught it as total silence.
80
+ if( responseText.includes( NO_MESSAGE_OPEN ) )
81
+ full.noMessage = extractTextBlock( responseText, NO_MESSAGE_TAG ) ?? '(no reason given)'
82
+
71
83
  return full
72
84
  }
73
85
 
@@ -193,6 +205,7 @@ function parseTaggedBlocks(
193
205
  'GOALS_REPRIORITIZE',
194
206
  'EFFECTORS',
195
207
  'SELF_OBS',
208
+ 'SKILLS',
196
209
  ],
197
210
  found = taggedTypes.filter( t => text.includes(`[${t}]`) )
198
211
  if( found.length > 0 ){
@@ -299,6 +312,13 @@ function parseTaggedBlocks(
299
312
  }
300
313
  catch { /* ignore */ }
301
314
 
315
+ // Named compound skills — the creation seam for learned composites (#114).
316
+ try {
317
+ const skillsData = parseJsonBlock('SKILLS') as { newSkills?: ExecutiveOutputFull['newSkills'] } | null
318
+ if( skillsData?.newSkills ) full.newSkills = skillsData.newSkills
319
+ }
320
+ catch { /* ignore */ }
321
+
302
322
  // [ACK] and legacy [REPLY] blocks are no longer emitted by any engine.
303
323
  // [ACK] — removed with the legacy master communication path (Phase 13.8).
304
324
  // [REPLY] — replaced by [REPLY_TEXT] plain-text block in conversation facets.