@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
package/src/llm/gate.ts CHANGED
@@ -19,11 +19,14 @@
19
19
  *
20
20
  * Configured via env vars:
21
21
  * WILL_LLM_CONCURRENCY max simultaneous LLM calls (default 2)
22
+ * WILL_LLM_RESPONSIVE_CONCURRENCY slots reserved for calls someone outside
23
+ * the mind is waiting on (default 1)
22
24
  * WILL_LLM_MAX_RETRIES retries before giving up on a 429 (default 4)
23
25
  * WILL_LLM_RETRY_BASE_MS first retry wait, doubles each attempt (default 2000)
24
26
  */
25
27
 
26
28
  import { logger } from '#core/logger'
29
+ import type { LLMCallFunction } from '#cognition/utilities/token.tracker'
27
30
 
28
31
  const MAX_CONCURRENT = parseInt( process.env.WILL_LLM_CONCURRENCY ?? '2')
29
32
 
@@ -76,6 +79,51 @@ export class LLMSemaphore {
76
79
 
77
80
  export const llmGate = new LLMSemaphore( MAX_CONCURRENT )
78
81
 
82
+ // ── The responsive lane ───────────────────────────────────────
83
+
84
+ /**
85
+ * A second gate, for calls someone OUTSIDE the mind is waiting on.
86
+ *
87
+ * `WILL_LLM_CONCURRENCY` ships with the comment "the minimum is 3: orbital,
88
+ * conversation, summary" — an intended allocation that one shared semaphore
89
+ * cannot enforce. Three slots means any three callers, and a mind deliberating
90
+ * at ~1,100 output tokens a call keeps all three warm indefinitely.
91
+ *
92
+ * Measured on a live COO over 7 hours: 29 of 45 replies began at or past the
93
+ * limit, and the worst spent 117 of its 131 seconds waiting for a slot rather
94
+ * than generating — 545 tokens at 4.2 tok/s against a provider that does ~39.
95
+ * From outside, that is a mind that read your message and said nothing.
96
+ *
97
+ * The distinction drawn here is NOT that conversation matters more. It is that
98
+ * something outside the mind is blocked on this call — a property of the call
99
+ * itself, which is why the predicate is named for the property and not for a
100
+ * list of blessed functions. Rumination can wait its turn. A person waiting for
101
+ * an answer cannot tell a mind that is thinking from one that is gone.
102
+ */
103
+ const RESPONSIVE_CONCURRENCY = parseInt( process.env.WILL_LLM_RESPONSIVE_CONCURRENCY ?? '1')
104
+
105
+ export const responsiveGate = new LLMSemaphore( RESPONSIVE_CONCURRENCY )
106
+
107
+ /**
108
+ * The cognitive functions something outside the mind is blocked on.
109
+ *
110
+ * `outreach` is deliberately absent: the mind started that one, and nobody is
111
+ * sitting on the other end waiting. Only work that a waiting party is already
112
+ * owed belongs in the reserved lane — widen this and the reservation stops
113
+ * meaning anything.
114
+ */
115
+ const AWAITED_OUTSIDE: ReadonlySet<LLMCallFunction> = new Set([ 'conversation' ])
116
+
117
+ /** Whether someone outside the mind is blocked on a call of this function. */
118
+ export function isAwaitedOutside( fn: LLMCallFunction | undefined ): boolean {
119
+ return fn !== undefined && AWAITED_OUTSIDE.has( fn )
120
+ }
121
+
122
+ /** The lane a call of this function belongs in. */
123
+ export function gateFor( fn: LLMCallFunction | undefined ): LLMSemaphore {
124
+ return isAwaitedOutside( fn ) ? responsiveGate : llmGate
125
+ }
126
+
79
127
  // ── Rate-limit detection ──────────────────────────────────────
80
128
 
81
129
  export function isRateLimitError( err: unknown ): boolean {
package/src/llm/index.ts CHANGED
@@ -8,11 +8,12 @@ import type { SessionLogger } from '#stem/tracts/session.logger'
8
8
  import { writeFileSync, mkdirSync } from 'node:fs'
9
9
  import type {
10
10
  TokenTracker, LLMCallCategory, LLMCallAttribute, LLMCallFunction,
11
+ LLMCallProcess,
11
12
  } from '#cognition/utilities/token.tracker'
12
13
  import { type ModelRouter, isNullRouter } from '#llm/routing'
13
14
  import { getCompletionRecorder, getCompletionSource } from '#core/completion.recorder'
14
15
  import type { LLMCompletionRecord } from '#core/completion.recorder'
15
- import { withGate } from '#llm/gate'
16
+ import { withGate, gateFor } from '#llm/gate'
16
17
  import { matchConversationFocus, wrapReplyText } from '#llm/wire.contracts'
17
18
 
18
19
  /**
@@ -263,6 +264,48 @@ export interface LLMCallResult {
263
264
  cacheWriteTok?: number
264
265
  }
265
266
 
267
+ /**
268
+ * The usage block on an Anthropic-wire SSE event. Every field is optional
269
+ * because which of them a host populates — and on which event — varies: see
270
+ * `takeUsage` in `_callAnthropicStream`.
271
+ */
272
+ export interface StreamUsage {
273
+ input_tokens?: number
274
+ output_tokens?: number
275
+ cache_read_input_tokens?: number
276
+ cache_creation_input_tokens?: number
277
+ }
278
+
279
+ /** Running token totals accumulated across a stream's usage events. */
280
+ export interface StreamTokens {
281
+ inputTok: number
282
+ outputTok: number
283
+ cacheReadTok: number
284
+ cacheWriteTok: number
285
+ }
286
+
287
+ /**
288
+ * Fold one SSE usage block into the running totals.
289
+ *
290
+ * Which event carries the real numbers is host-specific. Real Anthropic puts
291
+ * the input side on `message_start` and only `output_tokens` on `message_delta`.
292
+ * Z.ai (glm) sends `message_start` with every field zeroed and reports input,
293
+ * output and cache together on the final `message_delta`. Reading input from
294
+ * `message_start` alone recorded 0 input and 0 cache for every GLM call ever
295
+ * made — 441,896 output tokens against 0 input across one 7-hour COO run.
296
+ *
297
+ * So: fold from wherever it arrives, and let a later non-zero reading win. A
298
+ * zero never overwrites a figure already in hand, and a real figure always
299
+ * replaces a placeholder — which makes the fold correct under either ordering.
300
+ */
301
+ export function foldStreamUsage( acc: StreamTokens, u: StreamUsage ): StreamTokens {
302
+ if( u.input_tokens ) acc.inputTok = u.input_tokens
303
+ if( u.output_tokens ) acc.outputTok = u.output_tokens
304
+ if( u.cache_read_input_tokens ) acc.cacheReadTok = u.cache_read_input_tokens
305
+ if( u.cache_creation_input_tokens ) acc.cacheWriteTok = u.cache_creation_input_tokens
306
+ return acc
307
+ }
308
+
266
309
  /**
267
310
  * Cost-attribution metadata for a single LLM call. The same director instance is
268
311
  * shared by the master executive, every facet (conversation/planning/outreach),
@@ -275,6 +318,8 @@ export interface LLMCallMeta {
275
318
  /** The actor/subsystem doing the work. */
276
319
  attribute: LLMCallAttribute
277
320
  /** The specific cognitive function. */
321
+ process: LLMCallProcess
322
+ /** The specific cognitive function. */
278
323
  function: LLMCallFunction
279
324
  /** Optional specific id or namespace: facet id, entity id, model name. */
280
325
  scope?: string
@@ -284,13 +329,23 @@ export interface LLMCallMeta {
284
329
  * How much this call demands, 0..1 — MODEL_ROUTING W0.
285
330
  *
286
331
  * A *cognitive* measure, never a commercial one: it says how consequential or
287
- * uncertain this moment is, never who is paying for it. Two faculties already
288
- * compute it and simply forward what they have — the master and its facets
289
- * pass `effortScore` (the a-priori effort gate: uncertainty, prior
290
- * confidence, novelty, a pending reply, stress load), and deliberation passes
291
- * the agency stakes of the choice under consideration. Structurally
292
- * background work (summarising, guarding, embedding, delivery) reports a low
293
- * constant, because it is background whether the mind is calm or in crisis.
332
+ * uncertain this moment is, never who is paying for it.
333
+ *
334
+ * What actually reports it today:
335
+ * · the master and every facet including the deliberation facet — forward
336
+ * `effortScore`, the a-priori effort gate (uncertainty, prior confidence,
337
+ * novelty, a pending reply, stress load);
338
+ * · structurally background work (summarising, the identity guard) reports a
339
+ * low constant, because it is background whether the mind is calm or in
340
+ * crisis.
341
+ *
342
+ * Note what is NOT here: agency's `stakes( winner, bias )` — the consequence
343
+ * of the specific choice under contest — is computed in
344
+ * `agency/selection.scoring.ts` and never reaches a call. So a deliberation
345
+ * facet currently reports the tick's general effort, not the stakes of the
346
+ * choice it is deliberating. Carrying it is MODEL_ROUTING W1, still open.
347
+ * This comment previously claimed deliberation passed those stakes; it does
348
+ * not, and a doc comment that ships to npm is a bad place to be aspirational.
294
349
  *
295
350
  * Absent means UNKNOWN, not zero: a consumer must fall back to its default
296
351
  * rather than treat a missing value as "cheapest possible".
@@ -312,7 +367,7 @@ export const BACKGROUND_DEMAND = 0.1
312
367
  export const ESCALATION_DEMAND = 0.7
313
368
 
314
369
  /** Default attribution when a caller does not tag itself (back-compat). */
315
- const DEFAULT_CALL_META: LLMCallMeta = { category: 'executive', attribute: 'master', function: 'decision' }
370
+ const DEFAULT_CALL_META: LLMCallMeta = { category: 'executive', attribute: 'master', process: 'decision', function: '-' }
316
371
 
317
372
  /**
318
373
  * Fill in wire and base URL, or say clearly what is missing.
@@ -606,6 +661,9 @@ export class LLMDirector {
606
661
  // reached from several vendors at very different prices.
607
662
  model: ep.model,
608
663
  provider: ep.provider,
664
+ // The router's own input, kept alongside its output. Without this the
665
+ // ledger records WHERE a call went and never WHY.
666
+ demand: meta.demand,
609
667
  promptTokens: result.inputTok,
610
668
  completionTokens: result.outputTok,
611
669
  totalTokens: result.inputTok + result.outputTok,
@@ -613,6 +671,7 @@ export class LLMDirector {
613
671
  cacheWriteTokens: result.cacheWriteTok,
614
672
  category: meta.category,
615
673
  attribute: meta.attribute,
674
+ process: meta.process,
616
675
  function: meta.function,
617
676
  scope: meta.scope,
618
677
  label: meta.label,
@@ -686,11 +745,24 @@ export class LLMDirector {
686
745
  onChunk: ( chunk: string ) => void,
687
746
  temperature?: number,
688
747
  ): Promise<LLMCallResult> {
689
- // First-byte deadline only: a hard timeout on the whole stream would
690
- // truncate a healthy but long generation, so the timer is cleared once
691
- // the response headers arrive.
748
+ // IDLE deadline, not a whole-response one. A hard cap on the total stream
749
+ // would truncate a healthy long generation; no cap at all lets a stalled
750
+ // socket hang forever. A live generation keeps emitting and a dead one goes
751
+ // quiet, so the clock is restarted by every chunk that arrives and fires
752
+ // only after `_timeoutMs` of silence — the first byte included.
753
+ //
754
+ // It used to be cleared the moment the response headers arrived, which left
755
+ // the read loop with no deadline at all. A mid-stream stall then never
756
+ // settled, and the reasoning promise it belonged to stayed in the executive's
757
+ // `_pending` map forever: `hasPendingWork` gated every cycle and the master
758
+ // seat went silent for the full 600-tick prune horizon. Observed live as ten
759
+ // minutes of a mind not deliberating, announced by nothing.
692
760
  const controller = new AbortController()
693
- const timer = setTimeout( () => controller.abort(), this._timeoutMs )
761
+ let timer = setTimeout( () => controller.abort(), this._timeoutMs )
762
+ const restartDeadline = (): void => {
763
+ clearTimeout( timer )
764
+ timer = setTimeout( () => controller.abort(), this._timeoutMs )
765
+ }
694
766
 
695
767
  let res: Response
696
768
  try {
@@ -715,24 +787,23 @@ export class LLMDirector {
715
787
  throw err
716
788
  }
717
789
 
718
- clearTimeout( timer )
719
-
720
- if( !res.ok )
790
+ if( !res.ok ){
791
+ clearTimeout( timer )
721
792
  throw new Error(`Anthropic stream ${res.status}: ${( await res.text() ).slice(0, 300)}`)
793
+ }
722
794
 
723
795
  const reader = res.body!.getReader()
724
796
  const decoder = new TextDecoder()
725
797
  let buffer = ''
726
- let fullText = ''
727
- let inputTok = 0
728
- let outputTok = 0
729
- let cacheReadTok = 0
730
- let cacheWriteTok = 0
798
+ let fullText = ''
799
+ const tokens: StreamTokens = {
800
+ inputTok: 0, outputTok: 0, cacheReadTok: 0, cacheWriteTok: 0 }
731
801
 
732
802
  try {
733
803
  while( true ){
734
804
  const { done, value } = await reader.read()
735
805
  if( done ) break
806
+ restartDeadline()
736
807
  buffer += decoder.decode( value, { stream: true })
737
808
 
738
809
  const lines = buffer.split('\n')
@@ -747,32 +818,52 @@ export class LLMDirector {
747
818
  const ev = JSON.parse( raw ) as {
748
819
  type: string
749
820
  delta?: { type: string; text?: string; stop_reason?: string }
750
- message?: { usage?: { input_tokens: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number } }
751
- usage?: { output_tokens: number }
821
+ message?: { usage?: StreamUsage }
822
+ usage?: StreamUsage
752
823
  }
753
824
 
754
- if( ev.type === 'message_start' && ev.message?.usage ){
755
- inputTok = ev.message.usage.input_tokens
756
- cacheReadTok = ev.message.usage.cache_read_input_tokens ?? 0
757
- cacheWriteTok = ev.message.usage.cache_creation_input_tokens ?? 0
758
- }
825
+ // Usage arrives in different events depending on the host. Real
826
+ // Anthropic reports the input side up front on `message_start` and
827
+ // only `output_tokens` on `message_delta`. Anthropic-WIRE hosts do
828
+ // not all follow that: Z.ai (glm) sends `message_start` with zeroed
829
+ // placeholders and puts the true figures — input, output AND cache —
830
+ // on the final `message_delta`. Reading the input side from
831
+ // `message_start` alone therefore recorded 0 input and 0 cache for
832
+ // every GLM call ever made, while output logged correctly: 441,896
833
+ // output tokens against 0 input across one 7-hour COO run, which is
834
+ // not a possible shape for a conversation.
835
+ //
836
+ // So take usage wherever it appears and let a later non-zero reading
837
+ // win — a zero never overwrites a figure already in hand, and a real
838
+ // figure always replaces a placeholder.
839
+ if( ev.type === 'message_start' && ev.message?.usage )
840
+ foldStreamUsage( tokens, ev.message.usage )
759
841
  else if( ev.type === 'content_block_delta' && ev.delta?.text ){
760
842
  fullText += ev.delta.text
761
843
  onChunk( ev.delta.text )
762
844
  }
763
- else if( ev.type === 'message_delta' && ev.usage?.output_tokens ){
764
- outputTok = ev.usage.output_tokens
765
- }
845
+ else if( ev.type === 'message_delta' && ev.usage )
846
+ foldStreamUsage( tokens, ev.usage )
766
847
  }
767
848
  catch { /* ignore malformed events */ }
768
849
  }
769
850
  }
770
851
  }
852
+ catch( err ){
853
+ // An abort here is the idle deadline, not a caller cancelling: nothing
854
+ // arrived for `_timeoutMs` while the stream was still open. Naming it is
855
+ // the whole point — a stall that throws is a pass that failed, and a pass
856
+ // that failed settles its promise and frees the seat.
857
+ if( controller.signal.aborted )
858
+ throw new Error(`LLM stream to ${ep.provider} stalled — no data for ${this._timeoutMs}ms`)
859
+ throw err
860
+ }
771
861
  finally {
862
+ clearTimeout( timer )
772
863
  reader.releaseLock()
773
864
  }
774
865
 
775
- return { text: fullText, inputTok, outputTok, cacheReadTok, cacheWriteTok }
866
+ return { text: fullText, ...tokens }
776
867
  }
777
868
 
778
869
  /**
@@ -814,7 +905,10 @@ export class LLMDirector {
814
905
  () => ep.wire === 'anthropic'
815
906
  ? this._callAnthropicStream( ep, systemPrompt, userMessage, () => {}, temperature )
816
907
  : this._callProvider( ep, systemPrompt, userMessage, temperature ),
817
- 'executive/direct',
908
+ `executive/direct:${ meta.function }`,
909
+ // A reply to a waiting person takes the reserved lane; everything the
910
+ // mind is doing for itself shares the general one. See `gateFor`.
911
+ gateFor( meta.function ),
818
912
  )
819
913
 
820
914
  // Record token usage + cost into this Will's injected tracker (R4), tagged
@@ -99,6 +99,8 @@ export interface RoutingRule {
99
99
  category?: LLMCallMeta['category']
100
100
  /** Match `LLMCallMeta.attribute` exactly (e.g. 'master', 'facet', 'guard'). */
101
101
  attribute?: LLMCallMeta['attribute']
102
+ /** Match `LLMCallMeta.process` exactly (e.g. 'decision', 'ideation'). */
103
+ process?: LLMCallMeta['process']
102
104
  /** Match `LLMCallMeta.function` exactly (e.g. 'decision', 'consolidation'). */
103
105
  function?: LLMCallMeta['function']
104
106
  /**
@@ -182,6 +184,10 @@ export function chainRouters( ...routers: ( ModelRouter | null | undefined )[] )
182
184
  function matches( rule: RoutingRule, meta: LLMCallMeta ): boolean {
183
185
  if( rule.category !== undefined && rule.category !== meta.category ) return false
184
186
  if( rule.attribute !== undefined && rule.attribute !== meta.attribute ) return false
187
+ // `process` split out of `function` (ideation vs decision). It was declarable on a
188
+ // rule from the moment the axis existed but never tested here, so a rule that named
189
+ // it matched every process — the opposite of the AND semantics every other axis has.
190
+ if( rule.process !== undefined && rule.process !== meta.process ) return false
185
191
  if( rule.function !== undefined && rule.function !== meta.function ) return false
186
192
 
187
193
  // Absent demand is UNKNOWN, not zero: a demand-bounded rule cannot claim a
@@ -152,7 +152,7 @@ export class ExecutiveSummarizer {
152
152
  // MODEL_ROUTING W0 — compression is background work at a constant low
153
153
  // demand: distilling excerpts is the same job whether the mind is calm
154
154
  // or in crisis, so there is no honest per-tick measure to forward here.
155
- { category: 'summarizer', attribute: 'memory', function: 'consolidation', demand: BACKGROUND_DEMAND }
155
+ { category: 'summarizer', attribute: 'memory', process: 'cog', function: 'consolidation', demand: BACKGROUND_DEMAND }
156
156
  )
157
157
 
158
158
  if( result.text ){
@@ -34,11 +34,68 @@ export const REPLY_TEXT_TAG = 'REPLY_TEXT'
34
34
  export const REPLY_TEXT_OPEN = `[${REPLY_TEXT_TAG}]`
35
35
  export const REPLY_TEXT_CLOSE = `[/${REPLY_TEXT_TAG}]`
36
36
 
37
+ /**
38
+ * The block a facet uses to say it has decided NOT to speak, and why.
39
+ *
40
+ * "Omit [REPLY_TEXT] to stay silent" was stated in the prompt and nowhere else —
41
+ * a rule with no mechanism behind it. Live, an outreach facet read that it had
42
+ * three unanswered messages outstanding, correctly concluded there was nothing new
43
+ * to say, and — having no other block to put that sentence in — wrote it into
44
+ * REPLY_TEXT. "— nothing new to say to FKEM. Three messages unanswered is enough."
45
+ * was delivered to FKEM, in the third person, about him.
46
+ *
47
+ * The prompt already warned against exactly that. Warning was not enough, because
48
+ * the pressure is structural: a mind that decides not to speak still has something
49
+ * to say about the decision, and REPLY_TEXT was the only place to say it. This is
50
+ * the place. Content here is recorded and never sent.
51
+ */
52
+ export const NO_MESSAGE_TAG = 'NO_MESSAGE'
53
+ export const NO_MESSAGE_OPEN = `[${NO_MESSAGE_TAG}]`
54
+ export const NO_MESSAGE_CLOSE = `[/${NO_MESSAGE_TAG}]`
55
+
37
56
  /** Wrap a reply body in the block markers (the mock's emission shape). */
38
57
  export function wrapReplyText( body: string ): string {
39
58
  return [ REPLY_TEXT_OPEN, body, REPLY_TEXT_CLOSE ].join('\n')
40
59
  }
41
60
 
61
+ /**
62
+ * Every marker that is PROTOCOL rather than content.
63
+ *
64
+ * Named explicitly rather than matched as `[ANYTHING]`, because bracketed text is
65
+ * ordinary in real messages — "[1]", "[see §4.4]", "[REDACTED]" are things a mind
66
+ * may legitimately say, and a greedy strip would eat them.
67
+ */
68
+ export const PROTOCOL_TAGS: readonly string[] = [
69
+ REPLY_TEXT_TAG, NO_MESSAGE_TAG, 'INTROSPECTION', 'NARRATIVE', 'SELF_OBS',
70
+ ]
71
+
72
+ /**
73
+ * Remove any protocol marker that survived inside extracted content.
74
+ *
75
+ * `extractTextBlock` slices from the first `[TAG]` to the first `[/TAG]` after
76
+ * it, so a STRAY second opener inside the body is carried out as content — and
77
+ * the bubble splitter, seeing a line of its own, delivers it as a message.
78
+ *
79
+ * Live: a COO's reply to a technical document went out as four substantive
80
+ * bubbles followed by a fifth reading exactly `[REPLY_TEXT]`. The person got a
81
+ * message whose entire content was the name of the slot it should have filled.
82
+ *
83
+ * These tokens can never be legitimate content — they are the wire, not the
84
+ * words — so stripping them is not censorship of anything the mind meant. A line
85
+ * left empty by the removal is dropped so it cannot become an empty bubble.
86
+ */
87
+ export function stripProtocolMarkers( text: string ): string {
88
+ let out = text
89
+ for( const tag of PROTOCOL_TAGS )
90
+ out = out.split(`[${ tag }]`).join('').split(`[/${ tag }]`).join('')
91
+
92
+ return out
93
+ .split('\n')
94
+ .filter( ( line, i, all ) => line.trim() !== '' || ( i > 0 && i < all.length - 1 && all[ i - 1 ]?.trim() !== '') )
95
+ .join('\n')
96
+ .trim()
97
+ }
98
+
42
99
  // ── Conversation-facet focus — render ↔ match pair ────────────
43
100
  // The AuditionEngine RENDERS these lines into the facet focus; the mock LLM
44
101
  // MATCHES them to detect "this call is a conversation turn" and synthesize a
package/src/pma/index.ts CHANGED
@@ -32,6 +32,7 @@ import { readFileSync, existsSync } from 'node:fs'
32
32
  import { join } from 'node:path'
33
33
 
34
34
  import type { SimulationState } from '#core/types'
35
+ import { mergeEngineConfig } from '#cognition/config.mirror.entities'
35
36
  import type { DefaultSimulation } from '#core/simulation'
36
37
  import type { Cognition } from '#types'
37
38
  import type {
@@ -45,6 +46,7 @@ import {
45
46
  loadCompetence,
46
47
  type CompetenceSnapshot,
47
48
  } from '#agency/competence.codec'
49
+ import { mergeIdentity, composeIdentityPrompt, readPersona, IDENTITY_ENTITY_ID } from '#cognition/identity.entity'
48
50
 
49
51
  // ── Schema version ─────────────────────────────────────────────
50
52
  // Bump this when any field is removed or semantically changed.
@@ -280,7 +282,7 @@ export class PMADistiller {
280
282
  * @param willId The Will's ID
281
283
  * @param willName The Will's display name
282
284
  * @param state Current simulation state (from stateManager.snapshot())
283
- * @param sessionId Active session ID — recorded as provenance
285
+ * @param sessionId Active session ID — recorded on the snapshot as its origin
284
286
  * @param dataDir Root data dir (defaults to WILL_DATA_DIR env or './data')
285
287
  * @param repertoire The agency competence layer (in-memory manager). When
286
288
  * provided, the Will's learned skills + composite schemas are
@@ -361,7 +363,12 @@ export class PMADistiller {
361
363
  if( entity.type === 'will.identity'){
362
364
  const m = entity.metadata ?? {}
363
365
  return {
364
- prompt: ( m['prompt'] as string ) ?? '',
366
+ // The TENANT's text only. Capturing the composed `prompt` baked the
367
+ // container's WILL_CORE_PREAMBLE into the artifact, so a woken mind
368
+ // recited whichever build's preamble had distilled it — forever, and
369
+ // invisibly. `readPersona` falls back to stripping it out of a composed
370
+ // prompt, so artifacts written before the split load clean too.
371
+ prompt: readPersona( m ),
365
372
  values: ( m['values'] as string[] ) ?? [],
366
373
  traits: ( m['traits'] as Record<string, number> ) ?? {},
367
374
  traitStats: ( m['traitStats'] as PMAIdentity['traitStats'] ) ?? undefined,
@@ -792,22 +799,31 @@ export class PMALoader {
792
799
  const sm = simulation.stateManager
793
800
 
794
801
  // ── 1. Identity (with enhanced fields) ────────────────────
795
- sm.setEntity({
796
- id: 'identity-self',
797
- type: 'will.identity',
798
- createdAt: Date.now(),
799
- updatedAt: Date.now(),
800
- metadata: {
801
- prompt: pma.identity.prompt,
802
- values: pma.identity.values,
803
- traits: pma.identity.traits,
804
- traitStats: pma.identity.traitStats, // restore the Will's own norm (graded salience B/C)
805
- version: pma.identity.version,
806
- style: pma.identity.style,
807
- socialOrientation: pma.identity.socialOrientation,
808
- trustPropensity: pma.identity.trustPropensity,
809
- memoryPersistence: pma.identity.memoryPersistence,
810
- },
802
+ // MERGING (see cognition/identity.entity). An artifact carries what the mind
803
+ // LEARNED about itself; it does not carry the mind's name, because the name is
804
+ // supplied at boot by whoever is renting the container. This used to replace
805
+ // the entity outright, so `_seedIdentity`'s `name` — written moments earlier
806
+ // in the same wake — was erased before the first tick, every single wake.
807
+ // Recomposed, never restored verbatim: `pma.identity.prompt` is layer 2 (the
808
+ // persona), and layer 1 comes from the build that is running NOW. This is what
809
+ // makes a preamble fix reach minds that already exist.
810
+ const environment = sm.getEntity( IDENTITY_ENTITY_ID )?.metadata?.['environment']
811
+ // Through `readPersona`, NOT raw. An artifact written before the layers were
812
+ // split stores the COMPOSED prompt, so composing over it again gives the mind
813
+ // its own architecture twice. Putting this fallback only in the distiller was
814
+ // not enough — the loader is what every existing artifact goes through.
815
+ const persona = readPersona({ prompt: pma.identity.prompt })
816
+ mergeIdentity( sm, {
817
+ persona,
818
+ prompt: composeIdentityPrompt( persona, typeof environment === 'string' ? environment : undefined ),
819
+ values: pma.identity.values,
820
+ traits: pma.identity.traits,
821
+ traitStats: pma.identity.traitStats, // restore the Will's own norm (graded salience B/C)
822
+ version: pma.identity.version,
823
+ style: pma.identity.style,
824
+ socialOrientation: pma.identity.socialOrientation,
825
+ trustPropensity: pma.identity.trustPropensity,
826
+ memoryPersistence: pma.identity.memoryPersistence,
811
827
  })
812
828
 
813
829
  // ── 1b. Persona (learned self-tuning from the metacognition cycle) ──
@@ -889,55 +905,53 @@ export class PMALoader {
889
905
  const temperamentValence = pma.emotionalBaseline.temperamentValence
890
906
  const reactivity = pma.emotionalBaseline.reactivity
891
907
 
892
- sm.setEntity({
893
- id: 'engine-config-affective-blender',
894
- type: 'engine.config',
895
- createdAt: Date.now(),
896
- updatedAt: Date.now(),
897
- metadata: {
898
- engine: 'affective-blender',
899
- params: {
900
- inertia: 1 - reactivity,
901
- temperamentValence,
902
- },
908
+ // MERGE, never replace — same trap as engine-config-executive. The mirror
909
+ // seeds `emitBlendEvents` here and the PMA carries only `inertia`, so a
910
+ // whole-entity write silently dropped it for every restored Will.
911
+ mergeEngineConfig( sm, {
912
+ id: 'engine-config-affective-blender', engine: 'affective-blender',
913
+ params: {
914
+ inertia: 1 - reactivity,
915
+ temperamentValence,
903
916
  },
904
- })
917
+ }, 'incoming')
905
918
 
906
919
  // ── 6. Behavioral parameters ──────────────────────────────
907
920
  if( pma.behavioral.riskTolerance !== undefined ||
908
921
  pma.behavioral.explorationRate !== undefined ||
909
922
  pma.behavioral.impulsivity !== undefined ){
910
923
 
911
- sm.setEntity({
912
- id: 'engine-config-executive',
913
- type: 'engine.config',
914
- createdAt: Date.now(),
915
- updatedAt: Date.now(),
916
- metadata: {
917
- engine: 'executive',
918
- params: {
919
- riskTolerance: pma.behavioral.riskTolerance ?? 0.5,
920
- explorationRate: pma.behavioral.explorationRate ?? 0.3,
921
- impulsivity: pma.behavioral.impulsivity ?? 0.3,
922
- },
924
+ // MERGE onto the seeded mirror, never replace it.
925
+ //
926
+ // createWill() seeds engine-config-executive first (executiveInterval,
927
+ // cooldownTicks, deliberateThreshold, maxFacets); loadPMA() runs after. A
928
+ // whole-entity write here dropped every param the PMA does not carry — so
929
+ // for every Will restored from an artifact (i.e. every production Will), the
930
+ // executive's dual-process gate lost its base, `readBaseParams` returned
931
+ // nothing for `deliberateThreshold`, and consolidatePrior skipped its
932
+ // adjustments outright: the analytical/decisiveness edges that are supposed
933
+ // to develop how readily this mind stops to think had no base to move.
934
+ mergeEngineConfig( sm, {
935
+ id: 'engine-config-executive', engine: 'executive',
936
+ params: {
937
+ riskTolerance: pma.behavioral.riskTolerance ?? 0.5,
938
+ explorationRate: pma.behavioral.explorationRate ?? 0.3,
939
+ impulsivity: pma.behavioral.impulsivity ?? 0.3,
923
940
  },
924
- })
941
+ }, 'incoming')
925
942
  }
926
943
 
927
944
  // Configure memory persistence for ForgettingCurve
928
945
  if( pma.identity.memoryPersistence !== undefined ){
929
- sm.setEntity({
930
- id: 'engine-config-forgetting',
931
- type: 'engine.config',
932
- createdAt: Date.now(),
933
- updatedAt: Date.now(),
934
- metadata: {
935
- engine: 'forgetting-curve',
936
- params: {
937
- baseForgettingRate: 1 - ( pma.identity.memoryPersistence * 0.7 ),
938
- },
946
+ // MERGE — the mirror seeds emotionProtection / pruningThreshold /
947
+ // maxPrunePerTick here and the PMA carries only baseForgettingRate, so a
948
+ // whole-entity write dropped three params on every restore.
949
+ mergeEngineConfig( sm, {
950
+ id: 'engine-config-forgetting', engine: 'forgetting-curve',
951
+ params: {
952
+ baseForgettingRate: 1 - ( pma.identity.memoryPersistence * 0.7 ),
939
953
  },
940
- })
954
+ }, 'incoming')
941
955
  }
942
956
 
943
957
  // ── 7. Relationship stubs ─────────────────────────────────
@@ -86,3 +86,4 @@ export function auditAssemblyWiring( engines: readonly SimulationEngine[] ): Wir
86
86
  export function wiringKeys( records: WiringRecord[], status: WiringRecord['status'] ): string[] {
87
87
  return records.filter( r => r.status === status ).map( r => `${r.engine}.${r.method}`).sort()
88
88
  }
89
+
@@ -51,7 +51,7 @@ export interface IdentityReviewer {
51
51
  // MODEL_ROUTING W0 — a one-shot classification at creation time; constant low
52
52
  // demand (structurally background, and it runs before there is a mind whose
53
53
  // state could modulate it).
54
- const COHERENCE_META: LLMCallMeta = { category: 'identity-guard', attribute: 'guard', function: 'identity-coherence', demand: BACKGROUND_DEMAND }
54
+ const COHERENCE_META: LLMCallMeta = { category: 'identity-guard', attribute: 'guard', process: 'cog', function: 'identity-coherence', demand: BACKGROUND_DEMAND }
55
55
 
56
56
  const VALID_KINDS = new Set<CoherenceIssue['kind']>( [ 'contradiction', 'false-capability', 'injection', 'incoherence', 'other' ] )
57
57