@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
@@ -0,0 +1,204 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/llm/routing.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // MODEL_ROUTING W2 — per-call model selection.
6
+ //
7
+ // A Will runs one LLMDirector, and every call site shares it: the master's
8
+ // decision, each facet, the propose pass, the rolling summariser, the identity
9
+ // guard. They are not the same cognitive act, and they need not be the same
10
+ // inference. This module lets a host decide which model serves which call,
11
+ // without the engine learning anything it should not know.
12
+ //
13
+ // WHAT THIS IS NOT. This is a mechanism, never a policy. The engine may know
14
+ // that a call is routine or consequential — that is a cognitive fact it already
15
+ // computes (`LLMCallMeta.demand`). It must never know who is paying, what plan
16
+ // they are on, or what anything costs us. A router that needs commercial
17
+ // information is the host's to write; the seam below carries none of it.
18
+ //
19
+ // DETERMINISM CONTRACT. A router is an external oracle, exactly like the LLM
20
+ // itself and the policy arbiter. Its choice is already captured: the completion
21
+ // tape records `provider` and `model` on every call, and replay re-feeds the
22
+ // recorded completion rather than re-deciding. So replay never consults a
23
+ // router, and a run replays byte-for-byte whether the router is absent,
24
+ // present, or since reconfigured.
25
+ //
26
+ // SCOPE. A router may read the call meta and its own configuration, and nothing
27
+ // else. It must not reach into simulation state — that is what keeps this
28
+ // module below cognition (src/llm imports no cognition behaviour) and keeps
29
+ // routing from becoming a hidden input to the mind.
30
+ // ─────────────────────────────────────────────────────────────
31
+
32
+ import { logger } from '#core/logger'
33
+ import type { LLMProvider, LLMCallMeta } from '#llm/index'
34
+
35
+ /**
36
+ * Where a single call should go. Every field except `model` falls back to the
37
+ * Will's default when omitted.
38
+ */
39
+ export interface ModelRoute {
40
+ /**
41
+ * Omit to keep the Will's default provider and change only the model — the
42
+ * common "same vendor, different model for this kind of work" route, and what
43
+ * the per-role model map compiles to (a role has never had a provider of its
44
+ * own). Name a provider to cross vendors; it must appear in `llm.providers`
45
+ * or the route falls back to the default.
46
+ */
47
+ provider?: LLMProvider
48
+ model: string
49
+ /** Override the provider's API base (self-hosted / OpenAI-compatible servers). */
50
+ baseUrl?: string
51
+ /** Override the output-token ceiling for this call. */
52
+ maxOutputTokens?: number
53
+ }
54
+
55
+ /**
56
+ * Chooses a model for a call.
57
+ *
58
+ * Returning `null` means "no opinion" — the Will's default model is used. A
59
+ * router should return `null` rather than guess when it does not recognise a
60
+ * call: falling back is always safe, and a wrong route is not.
61
+ */
62
+ export interface ModelRouter {
63
+ /** Stable identifier, recorded alongside routing telemetry. */
64
+ readonly name: string
65
+ route( meta: LLMCallMeta ): ModelRoute | null
66
+ }
67
+
68
+ /**
69
+ * The default. Has no opinion about anything, allocates nothing.
70
+ *
71
+ * A Will running this must be byte-identical to one built before the routing
72
+ * seam existed — that property is asserted by test, and it is what lets this
73
+ * ship dark.
74
+ */
75
+ export const NULL_ROUTER: ModelRouter = {
76
+ name: 'null',
77
+ route(): ModelRoute | null { return null },
78
+ }
79
+
80
+ /** True when the router is the no-op default (used to skip the seam entirely). */
81
+ export function isNullRouter( router: ModelRouter | null | undefined ): boolean {
82
+ return !router || router === NULL_ROUTER
83
+ }
84
+
85
+ // ── Reference implementation ──────────────────────────────────
86
+
87
+ /**
88
+ * One entry in a {@link TableRouter}'s table. All present conditions must match
89
+ * (logical AND); an absent condition matches anything.
90
+ */
91
+ export interface RoutingRule {
92
+ /**
93
+ * Match `LLMCallMeta.category` exactly (e.g. 'executive', 'summarizer').
94
+ *
95
+ * The axes are typed rather than free strings so a rule that names a bucket
96
+ * the engine never emits fails to compile instead of silently never matching
97
+ * — a routing table's worst failure is the rule that looks right and is dead.
98
+ */
99
+ category?: LLMCallMeta['category']
100
+ /** Match `LLMCallMeta.attribute` exactly (e.g. 'master', 'facet', 'guard'). */
101
+ attribute?: LLMCallMeta['attribute']
102
+ /** Match `LLMCallMeta.process` exactly (e.g. 'decision', 'ideation'). */
103
+ process?: LLMCallMeta['process']
104
+ /** Match `LLMCallMeta.function` exactly (e.g. 'decision', 'consolidation'). */
105
+ function?: LLMCallMeta['function']
106
+ /**
107
+ * Inclusive lower bound on `LLMCallMeta.demand`. A call with no demand
108
+ * reported never matches a rule that sets this — absent means unknown, and
109
+ * unknown must not be treated as zero.
110
+ */
111
+ minDemand?: number
112
+ /** Exclusive upper bound on `LLMCallMeta.demand`. Same absence rule. */
113
+ maxDemand?: number
114
+ /** Where a matching call goes. */
115
+ route: ModelRoute
116
+ }
117
+
118
+ /**
119
+ * A worked example of the seam: first matching rule wins, otherwise no opinion.
120
+ *
121
+ * This ships so that the interface has a reference implementation and so that
122
+ * hosts have something to copy — it is deliberately dumb. It is not a routing
123
+ * strategy, and the engine ships no table of its own: what belongs where is the
124
+ * host's decision, expressed as configuration.
125
+ *
126
+ * Rules are evaluated in order, so put specific rules before general ones.
127
+ */
128
+ export class TableRouter implements ModelRouter {
129
+ readonly name: string
130
+ private readonly _rules: readonly RoutingRule[]
131
+
132
+ constructor( rules: readonly RoutingRule[], name = 'table' ){
133
+ this._rules = [ ...rules ]
134
+ this.name = name
135
+ }
136
+
137
+ route( meta: LLMCallMeta ): ModelRoute | null {
138
+ for( const rule of this._rules ){
139
+ if( matches( rule, meta ) ) return rule.route
140
+ }
141
+ return null
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Ask each router in turn; the first with an opinion wins.
147
+ *
148
+ * This exists because a Will can have two sources of routing at once: the
149
+ * host's own router, and the one compiled from its per-role model map. Order
150
+ * expresses precedence — an explicit router is consulted before the role map,
151
+ * which is the precedence those two mechanisms already had when roles were
152
+ * served by separate directors.
153
+ *
154
+ * A throwing link is skipped, not propagated. The links are independent
155
+ * decisions, and one broken router must not take a working one down with it —
156
+ * that would silently demote every role-mapped call to the default model.
157
+ */
158
+ export function chainRouters( ...routers: ( ModelRouter | null | undefined )[] ): ModelRouter {
159
+ const chain = routers.filter( ( r ): r is ModelRouter => !isNullRouter( r ) )
160
+ if( chain.length === 0 ) return NULL_ROUTER
161
+ if( chain.length === 1 ) return chain[ 0 ]!
162
+
163
+ const warned = new Set<string>()
164
+ return {
165
+ name: chain.map( r => r.name ).join('>'),
166
+ route( meta: LLMCallMeta ): ModelRoute | null {
167
+ for( const router of chain ){
168
+ try {
169
+ const hit = router.route( meta )
170
+ if( hit ) return hit
171
+ }
172
+ catch( err ){
173
+ if( !warned.has( router.name ) ){
174
+ warned.add( router.name )
175
+ logger.warn(`[llm.routing] router "${router.name}" threw — skipping it: ${( err as Error ).message}`)
176
+ }
177
+ }
178
+ }
179
+ return null
180
+ },
181
+ }
182
+ }
183
+
184
+ function matches( rule: RoutingRule, meta: LLMCallMeta ): boolean {
185
+ if( rule.category !== undefined && rule.category !== meta.category ) return false
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
191
+ if( rule.function !== undefined && rule.function !== meta.function ) return false
192
+
193
+ // Absent demand is UNKNOWN, not zero: a demand-bounded rule cannot claim a
194
+ // call whose demand was never measured.
195
+ const bounded = rule.minDemand !== undefined || rule.maxDemand !== undefined
196
+ if( bounded ){
197
+ const d = meta.demand
198
+ if( typeof d !== 'number' || Number.isNaN( d ) ) return false
199
+ if( rule.minDemand !== undefined && d < rule.minDemand ) return false
200
+ if( rule.maxDemand !== undefined && d >= rule.maxDemand ) return false
201
+ }
202
+
203
+ return true
204
+ }
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { logger } from '#core/logger'
19
19
  import type { LLMDirector } from '#llm/index'
20
+ import { BACKGROUND_DEMAND } from '#llm/index'
20
21
 
21
22
  export interface SummarizerConfig {
22
23
  /** How many executive calls between summarization runs. Default: 10 */
@@ -148,7 +149,10 @@ export class ExecutiveSummarizer {
148
149
  userMessage,
149
150
  this._callCount as any,
150
151
  undefined,
151
- { category: 'summarizer', attribute: 'memory', function: 'consolidation' }
152
+ // MODEL_ROUTING W0 compression is background work at a constant low
153
+ // demand: distilling excerpts is the same job whether the mind is calm
154
+ // or in crisis, so there is no honest per-tick measure to forward here.
155
+ { category: 'summarizer', attribute: 'memory', process: 'cog', function: 'consolidation', demand: BACKGROUND_DEMAND }
152
156
  )
153
157
 
154
158
  if( result.text ){
@@ -34,6 +34,25 @@ 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')
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.
@@ -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 ─────────────────────────────────
@@ -16,6 +16,14 @@
16
16
 
17
17
  import type { WillConfig } from '#stem/mind'
18
18
  import type { StateSnapshot } from '#stem/index'
19
+ import type { TokenTracker } from '#cognition/utilities/token.tracker'
20
+
21
+ /**
22
+ * The Will's token tracker, for cost display. Held here rather than passed in
23
+ * because printStatus is registered as a TickListener, whose third parameter is
24
+ * the outbox. Cost is no longer a state metric (W8c) — it lives on the tracker.
25
+ */
26
+ let _tracker: TokenTracker | undefined
19
27
  import { WillStem } from '#stem/index'
20
28
 
21
29
  // ── Env config ────────────────────────────────────────────────
@@ -88,14 +96,15 @@ function printStatus( snapshot: StateSnapshot, tick: number ): void {
88
96
  ` entities=${entities}`
89
97
  )
90
98
 
91
- // LLM stats every LOG_INTERVAL
92
- const costTotal = ( snapshot.metrics.get('llm.cost_total_usd') ?? 0 ).toFixed( 4 )
93
- const costThisTick = ( snapshot.metrics.get('llm.cost_this_tick_usd') ?? 0 ).toFixed( 5 )
94
- const totalCalls = snapshot.metrics.get('llm.total_calls') ?? 0
99
+ // LLM stats every LOG_INTERVAL. Tokens come from state; cost comes from the
100
+ // tracker dollars are host accounting and no longer live in state (W8c).
101
+ const totalCalls = snapshot.metrics.get('llm.total_calls') ?? 0
102
+ const cost = _tracker
103
+ ? ` total_cost=$${_tracker.totalCostUsd.toFixed( 4 )}`
104
+ : ''
95
105
 
96
106
  console.log(
97
- ` [llm] total_cost=$${costTotal}` +
98
- ` tick_cost=$${costThisTick}` +
107
+ ` [llm]${cost}` +
99
108
  ` calls=${totalCalls}` +
100
109
  ` prompt=${snapshot.metrics.get('llm.prompt_tokens_total') ?? 0}` +
101
110
  ` completion=${snapshot.metrics.get('llm.completion_tokens_total') ?? 0}`
@@ -141,6 +150,9 @@ async function main(): Promise<void> {
141
150
  console.log('[init] Assembling mind...')
142
151
  await manager.createWill( willConfig )
143
152
 
153
+ // Cost is no longer a state metric (W8c) — read it off the Will's tracker.
154
+ _tracker = manager.getWillCognition( WILL_ID ).tokenTracker
155
+
144
156
  manager.addTickListener( WILL_ID, printStatus )
145
157
 
146
158
  // Graceful shutdown