@mindot/will 0.1.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 (178) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +777 -0
  3. package/dist/index.d.ts +8144 -0
  4. package/dist/index.js +26231 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +73 -0
  7. package/src/cognition/agency/access.grants.ts +59 -0
  8. package/src/cognition/agency/competence.codec.ts +89 -0
  9. package/src/cognition/agency/engines/action.selector.ts +462 -0
  10. package/src/cognition/agency/engines/affordance.synthesizer.ts +384 -0
  11. package/src/cognition/agency/engines/deliberation.engine.ts +241 -0
  12. package/src/cognition/agency/engines/instruction.intake.ts +54 -0
  13. package/src/cognition/agency/engines/motor.schema.executor.ts +532 -0
  14. package/src/cognition/agency/engines/reafference.engine.ts +241 -0
  15. package/src/cognition/agency/execution.primitives.ts +118 -0
  16. package/src/cognition/agency/index.ts +58 -0
  17. package/src/cognition/agency/proactive.communicator.ts +274 -0
  18. package/src/cognition/agency/reconcile.learning.ts +82 -0
  19. package/src/cognition/agency/schemas/external.ts +59 -0
  20. package/src/cognition/agency/schemas/innate.ts +111 -0
  21. package/src/cognition/agency/schemas/repertoire.ts +246 -0
  22. package/src/cognition/agency/selection.scoring.ts +191 -0
  23. package/src/cognition/agency/types.ts +165 -0
  24. package/src/cognition/bus.ts +391 -0
  25. package/src/cognition/completion.inbox.ts +86 -0
  26. package/src/cognition/config.mirror.entities.ts +475 -0
  27. package/src/cognition/conversation.memory.ts +81 -0
  28. package/src/cognition/event.log.ts +116 -0
  29. package/src/cognition/event.schemas.ts +568 -0
  30. package/src/cognition/faculties/aesthetic.evaluator.ts +313 -0
  31. package/src/cognition/faculties/affective.blender.ts +562 -0
  32. package/src/cognition/faculties/attachment.evaluator.ts +432 -0
  33. package/src/cognition/faculties/attention.allocator.ts +422 -0
  34. package/src/cognition/faculties/autobiographical.narrator.ts +300 -0
  35. package/src/cognition/faculties/bias.detector.ts +363 -0
  36. package/src/cognition/faculties/circadian.oscillator.ts +294 -0
  37. package/src/cognition/faculties/confidence.calibrator.ts +324 -0
  38. package/src/cognition/faculties/dream.simulator.ts +251 -0
  39. package/src/cognition/faculties/empathy.simulator.ts +253 -0
  40. package/src/cognition/faculties/energy.regulator.ts +320 -0
  41. package/src/cognition/faculties/episodic.consolidator.ts +728 -0
  42. package/src/cognition/faculties/executive.engine/commands.ts +459 -0
  43. package/src/cognition/faculties/executive.engine/config.ts +46 -0
  44. package/src/cognition/faculties/executive.engine/context.ts +605 -0
  45. package/src/cognition/faculties/executive.engine/deferred.effects.ts +104 -0
  46. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +60 -0
  47. package/src/cognition/faculties/executive.engine/effort.gate.ts +114 -0
  48. package/src/cognition/faculties/executive.engine/engine.ts +1028 -0
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +90 -0
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +271 -0
  51. package/src/cognition/faculties/executive.engine/facet.ts +628 -0
  52. package/src/cognition/faculties/executive.engine/gating.ts +222 -0
  53. package/src/cognition/faculties/executive.engine/index.ts +6 -0
  54. package/src/cognition/faculties/executive.engine/messages.ts +102 -0
  55. package/src/cognition/faculties/executive.engine/parser.ts +380 -0
  56. package/src/cognition/faculties/executive.engine/prompt.factory.ts +1053 -0
  57. package/src/cognition/faculties/executive.engine/types.ts +323 -0
  58. package/src/cognition/faculties/exteroception.ts +338 -0
  59. package/src/cognition/faculties/forgetting.curve.ts +202 -0
  60. package/src/cognition/faculties/frustration.evaluator.ts +280 -0
  61. package/src/cognition/faculties/goal.manager.ts +1008 -0
  62. package/src/cognition/faculties/inhibition.controller.ts +351 -0
  63. package/src/cognition/faculties/interoception.ts +417 -0
  64. package/src/cognition/faculties/introspection.engine.ts +246 -0
  65. package/src/cognition/faculties/known.entity.tracker.ts +439 -0
  66. package/src/cognition/faculties/loss.evaluator.ts +224 -0
  67. package/src/cognition/faculties/moral.evaluator.ts +437 -0
  68. package/src/cognition/faculties/novelty.detector.ts +254 -0
  69. package/src/cognition/faculties/persona.consolidator.ts +845 -0
  70. package/src/cognition/faculties/planning.engine/engine.ts +859 -0
  71. package/src/cognition/faculties/planning.engine/plan.frontier.ts +83 -0
  72. package/src/cognition/faculties/planning.engine/plan.store.ts +174 -0
  73. package/src/cognition/faculties/planning.engine/plan.supervision.ts +494 -0
  74. package/src/cognition/faculties/planning.engine/types.ts +143 -0
  75. package/src/cognition/faculties/reputation.tracker.ts +294 -0
  76. package/src/cognition/faculties/reward.evaluator.ts +304 -0
  77. package/src/cognition/faculties/self.model.updater.ts +677 -0
  78. package/src/cognition/faculties/semantic.engine/clustering.ts +673 -0
  79. package/src/cognition/faculties/semantic.engine/index.ts +6 -0
  80. package/src/cognition/faculties/semantic.engine/integrator.ts +750 -0
  81. package/src/cognition/faculties/semantic.engine/types.ts +53 -0
  82. package/src/cognition/faculties/sleep.pressure.regulator.ts +262 -0
  83. package/src/cognition/faculties/social.perception.ts +333 -0
  84. package/src/cognition/faculties/spaced.repetition.ts +605 -0
  85. package/src/cognition/faculties/stress.regulator.ts +375 -0
  86. package/src/cognition/faculties/task.switcher.ts +239 -0
  87. package/src/cognition/faculties/theory.of.mind.ts +333 -0
  88. package/src/cognition/faculties/threat.evaluator.ts +312 -0
  89. package/src/cognition/faculties/working.memory.ts +406 -0
  90. package/src/cognition/generative.model.ts +344 -0
  91. package/src/cognition/heartbeat.ts +70 -0
  92. package/src/cognition/index.ts +291 -0
  93. package/src/cognition/instruction.handler.ts +146 -0
  94. package/src/cognition/memory/index.ts +35 -0
  95. package/src/cognition/memory/vector.adapter.ts +316 -0
  96. package/src/cognition/memory/vector.content.ts +47 -0
  97. package/src/cognition/memory/vector.embedder.ts +212 -0
  98. package/src/cognition/memory/vector.index.ts +459 -0
  99. package/src/cognition/memory/vector.types.ts +65 -0
  100. package/src/cognition/orchestrator.ts +142 -0
  101. package/src/cognition/persona.prior.ts +265 -0
  102. package/src/cognition/schema.registry.ts +145 -0
  103. package/src/cognition/senses/audition.engine/engine.ts +976 -0
  104. package/src/cognition/senses/audition.engine/salience.ts +59 -0
  105. package/src/cognition/senses/base.sense.engine.ts +117 -0
  106. package/src/cognition/senses/gustation.engine.ts +24 -0
  107. package/src/cognition/senses/index.ts +172 -0
  108. package/src/cognition/senses/olfaction.engine.ts +24 -0
  109. package/src/cognition/senses/somatosensation.engine.ts +23 -0
  110. package/src/cognition/senses/vision.engine.ts +23 -0
  111. package/src/cognition/types.ts +63 -0
  112. package/src/cognition/utilities/token.tracker.ts +492 -0
  113. package/src/core/abstracts.ts +60 -0
  114. package/src/core/async.engine.ts +539 -0
  115. package/src/core/clock.ts +196 -0
  116. package/src/core/completion.recorder.ts +122 -0
  117. package/src/core/conflict.detector.ts +140 -0
  118. package/src/core/distributed.ts +626 -0
  119. package/src/core/event.bus.ts +292 -0
  120. package/src/core/inbound.recorder.ts +100 -0
  121. package/src/core/index.ts +167 -0
  122. package/src/core/logger.ts +85 -0
  123. package/src/core/metrics.ts +107 -0
  124. package/src/core/orchestrator.ts +625 -0
  125. package/src/core/replay.ts +589 -0
  126. package/src/core/scenario.ts +120 -0
  127. package/src/core/serialization.ts +448 -0
  128. package/src/core/simulation.ts +197 -0
  129. package/src/core/snapshot.manager.ts +293 -0
  130. package/src/core/state.manager.ts +283 -0
  131. package/src/core/types.ts +274 -0
  132. package/src/core/utils.ts +82 -0
  133. package/src/core/wall.clock.ts +23 -0
  134. package/src/extensions/livestream.ext.ts +570 -0
  135. package/src/extensions/time.ext.ts +339 -0
  136. package/src/index.ts +91 -0
  137. package/src/llm/gate.ts +140 -0
  138. package/src/llm/index.ts +716 -0
  139. package/src/llm/summarizer.ts +170 -0
  140. package/src/llm/wire.contracts.ts +67 -0
  141. package/src/pma/eval.ts +651 -0
  142. package/src/pma/index.ts +1082 -0
  143. package/src/profiles/built-in.ts +7 -0
  144. package/src/profiles/companion.ts +28 -0
  145. package/src/profiles/company-brain.ts +54 -0
  146. package/src/profiles/customer-service.ts +30 -0
  147. package/src/profiles/game-npc.ts +20 -0
  148. package/src/profiles/index.ts +42 -0
  149. package/src/profiles/smart-home.ts +34 -0
  150. package/src/runners/coherence.runner.ts +49 -0
  151. package/src/runners/outreach.runner.ts +158 -0
  152. package/src/runners/social.runner.ts +182 -0
  153. package/src/runners/thin-shim.runner.ts +195 -0
  154. package/src/sdk/will.ts +334 -0
  155. package/src/stem/assembly.audit.ts +88 -0
  156. package/src/stem/distribution.ts +203 -0
  157. package/src/stem/guards/identity.coherence.ts +152 -0
  158. package/src/stem/guards/identity.guard.ts +227 -0
  159. package/src/stem/index.ts +1145 -0
  160. package/src/stem/mind.ts +1090 -0
  161. package/src/stem/tracts/ack.reconciler.ts +55 -0
  162. package/src/stem/tracts/biography.writer.ts +190 -0
  163. package/src/stem/tracts/effector.controller.ts +143 -0
  164. package/src/stem/tracts/health.reporter.ts +113 -0
  165. package/src/stem/tracts/inbound.queue.ts +54 -0
  166. package/src/stem/tracts/outbox.controller.ts +167 -0
  167. package/src/stem/tracts/outbox.writer.ts +133 -0
  168. package/src/stem/tracts/pma.controller.ts +109 -0
  169. package/src/stem/tracts/replay.controller.ts +177 -0
  170. package/src/stem/tracts/sensory.controller.ts +168 -0
  171. package/src/stem/tracts/session.logger.ts +157 -0
  172. package/src/stem/tracts/transport/index.ts +38 -0
  173. package/src/stem/tracts/transport/loopback.transport.ts +86 -0
  174. package/src/stem/tracts/transport/socketio.transport.ts +200 -0
  175. package/src/stem/tracts/transport/stream.transport.ts +152 -0
  176. package/src/stem/tracts/transport/types.ts +193 -0
  177. package/src/stem/tracts/transport.controller.ts +380 -0
  178. package/src/types.ts +116 -0
@@ -0,0 +1,1008 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/faculties/goal.manager.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * GoalManager — maintains the active goal hierarchy.
7
+ *
8
+ * Manages:
9
+ * - Goal activation and deactivation based on drives and context
10
+ * - Goal priority ordering (conflict resolution)
11
+ * - Progress tracking
12
+ * - Goal completion and abandonment
13
+ * - Sub-goal decomposition
14
+ *
15
+ * Goals are entities in the state manager. The GoalManager reads
16
+ * drive signals from regulatory engines and perceptual context to
17
+ * determine which goals should be active, then updates their
18
+ * priorities and tracks progress.
19
+ *
20
+ * Part of Shard 3 (Executive Layer) — runs every tick, synchronous.
21
+ */
22
+
23
+ import type {
24
+ Duration,
25
+ Tick,
26
+ SimulationContext,
27
+ ReadonlySimulationState,
28
+ StateCommands,
29
+ SimulationEvent,
30
+ } from '#core/types'
31
+ import type { SimulationEngine, EngineResult, CognitiveEngine } from '#cognition/types'
32
+ import type { CognitiveEventSchema } from '#cognition/schema.registry'
33
+ import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
34
+ import type { SessionLogger } from '#stem/tracts/session.logger'
35
+ import { readEffectiveParams } from '#cognition/persona.prior'
36
+ import { GenerativeModel } from '#cognition/generative.model'
37
+
38
+ export interface GoalManagerConfig {
39
+ /** Maximum active goals at once */
40
+ maxActiveGoals?: number
41
+ /** How quickly goal priority decays without reinforcement */
42
+ priorityDecayRate?: number
43
+ /** Minimum priority before a goal is deactivated */
44
+ deactivationThreshold?: number
45
+ /**
46
+ * Resilience / grit. A goal whose priority ≥ gritPriority is NEVER auto-abandoned
47
+ * by staleness — the mind only lets it go by a deliberate executive decision.
48
+ * Below that, the staleness patience window scales with priority by
49
+ * gritPatienceScale (important stuck goals are pursued much longer).
50
+ */
51
+ gritPriority?: number
52
+ gritPatienceScale?: number
53
+ /** Frustration tolerance [0–1]: how much the frustration emotion is allowed to
54
+ * compress the patience window. High tolerance → setbacks don't make the mind
55
+ * give up faster. A personality dimension (seeded by PMA, developed by metacog). */
56
+ frustrationTolerance?: number
57
+ /**
58
+ * How many new beliefs must form after a goal activates for it to be
59
+ * considered complete. Applies only to epistemic (non-drive) goals.
60
+ */
61
+ epistemicBeliefThreshold?: number
62
+ bus?: CognitiveBus
63
+ }
64
+
65
+ /** Drive-related tags — used to recognise goals spawned by drive signals. */
66
+ const DRIVE_TAGS = new Set([ 'energy', 'sleep', 'stress', 'survival', 'wellbeing', 'external', 'demand' ])
67
+
68
+ // Task-persistence commitment (Channel A). The goal task.switcher is focused on gets a
69
+ // bounded priority boost that BUILDS the longer it's been focused, is AMPLIFIED by sunk
70
+ // cost in an in-progress plan (planning.engine), and SCALES with the switch cost (= the
71
+ // conscientiousness-developable baseSwitchCost, #28). Recomputed fresh each tick from
72
+ // basePriority — never accumulates. This is what makes the focus mechanically "stick":
73
+ // the focused goal stays top for goal selection, the executive, and planning.
74
+ const FOCUS_COMMITMENT_RAMP = 30 // ticks of focus to reach full commitment weight
75
+ const COMMITMENT_GAIN = 0.3 // scales switchCost × commitment × plan-sunk-cost
76
+ const MAX_COMMITMENT_BOOST = 0.2 // cap — a clearly higher-priority goal still wins
77
+
78
+
79
+ export interface GoalState {
80
+ id: string
81
+ description: string
82
+ priority: number // 0-1: current priority
83
+ basePriority: number // 0-1: priority from drives (stable)
84
+ progress: number // 0-1
85
+ status: 'active' | 'blocked' | 'completed' | 'abandoned' | 'pending' | 'pending_verification'
86
+ parentGoalId?: string
87
+ subGoals: string[]
88
+ activatedAt: Tick
89
+ deadline?: Tick
90
+ tags: string[]
91
+ /** Snapshot of memory.beliefs_total when this goal was activated.
92
+ * Used to compute epistemic progress: (currentBeliefs - baseline) / threshold. */
93
+ beliefsAtActivation: number
94
+
95
+ /**
96
+ * How this goal knows it is done — set at creation time, not inferred from tags.
97
+ *
98
+ * 'metric' — a measurable state crosses a threshold (completionCondition).
99
+ * 'action' — a real-world outcome must occur; stays 0% if impossible and
100
+ * is abandoned through frustration rather than belief formation.
101
+ * 'epistemic' — resolved through understanding: belief formation about the
102
+ * situation, the self, or the world.
103
+ */
104
+ completionType: 'metric' | 'action' | 'epistemic' | 'pending_verification'
105
+
106
+ /** For 'metric' goals: e.g. "stress.load < 40" or "energy.level > 80".
107
+ * Parsed and evaluated each tick to compute smooth progress. */
108
+ completionCondition?: string
109
+
110
+ /** Tick of the most recent action.outcome that matched this goal's tags.
111
+ * Lets the executive see "I tried this N ticks ago" without scanning decision.records. */
112
+ lastActionAttemptTick?: number
113
+ /** ActionType of the most recent attempt (e.g. 'text', 'talk', 'observe'). */
114
+ lastActionType?: string
115
+
116
+ /**
117
+ * Causal link back to the entity whose message triggered this goal.
118
+ * Populated when a conversation escalation creates the goal (AuditionEngine → executive).
119
+ * Used by PlanningEngine to tag plan bus events so the activity SSE stream can
120
+ * filter and forward them to the correct requesting entity.
121
+ */
122
+ requestingEntityId?: string
123
+ /** Thread ID of the conversation turn that triggered this goal (for reply correlation). */
124
+ requestingThreadId?: string
125
+ }
126
+
127
+ export class GoalManager implements SimulationEngine, CognitiveEngine {
128
+ readonly name = 'goal-manager'
129
+
130
+ private _maxActiveGoals: number
131
+ private _priorityDecayRate: number
132
+ private _basePriorityDecayRate: number
133
+ private _deactivationThreshold: number
134
+ private _gritPriority: number
135
+ private _gritPatienceScale: number
136
+ private _frustrationTolerance: number
137
+ private _epistemicBeliefThreshold: number
138
+
139
+ private _goals = new Map<string, GoalState>()
140
+ private _goalCounter = 0
141
+ /** Updated every tick — used as the baseline for newly created goals. */
142
+ private _currentBeliefCount = 0
143
+ /** IDs of goals for which goal.achieved has already been published, so we
144
+ * don't re-fire the event on every subsequent tick. */
145
+ private _achievedGoalIds = new Set<string>()
146
+ /** IDs of goals for which goal.completed has already been emitted. Same
147
+ * once-per-goal guard as _achievedGoalIds — completedGoals accumulates across
148
+ * ticks, so without this the event re-fires every tick for every completed
149
+ * goal (the goal-completion churn). */
150
+ private _completedEmittedIds = new Set<string>()
151
+
152
+ // ── Blocked-goal detection ─────────────────────────────────
153
+ private _goalLastProgress = new Map<string, number>() // goalId → last progress
154
+ private _goalStuckSince = new Map<string, Tick>() // goalId → tick stuck began
155
+
156
+ private _energyLevel: number = 100
157
+ private _sleepPressure: number = 0
158
+ private _stressLoad: number = 0
159
+ private _executiveGoalConfidence: number = 0.5
160
+
161
+ /** Tracks current simulation tick so addGoal() can stamp activatedAt correctly. */
162
+ private _currentTick: Tick = 0
163
+
164
+ private _bus: CognitiveBus | null = null
165
+ private _sessionLogger: SessionLogger | null = null
166
+
167
+ private readonly _model = new GenerativeModel()
168
+
169
+
170
+ attachSessionLogger( logger: SessionLogger | null ): void {
171
+ this._sessionLogger = logger
172
+ }
173
+
174
+ constructor( config: GoalManagerConfig = {} ){
175
+ this._bus = config.bus ?? null
176
+ this._maxActiveGoals = config.maxActiveGoals ?? 5
177
+ this._priorityDecayRate = config.priorityDecayRate ?? 0.005
178
+ this._basePriorityDecayRate = this._priorityDecayRate
179
+ this._deactivationThreshold = config.deactivationThreshold ?? 0.1
180
+ this._gritPriority = config.gritPriority ?? 0.8
181
+ this._gritPatienceScale = config.gritPatienceScale ?? 2
182
+ this._frustrationTolerance = config.frustrationTolerance ?? 0.5
183
+ this._epistemicBeliefThreshold = config.epistemicBeliefThreshold ?? 8
184
+ }
185
+ attachBus( bus: CognitiveBus ): void { this._bus = bus }
186
+
187
+ // ── Engine interface ─────────────────────────────────────
188
+
189
+ private readonly _STUCK_THRESHOLD = 20 // ticks without progress before "blocked"
190
+
191
+ subscribes(): string[] {
192
+ return [
193
+ 'energy.state.changed',
194
+ 'sleep.state.changed',
195
+ 'stress.state.changed',
196
+ 'belief.updated',
197
+ 'executive.goal.proposed',
198
+ 'executive.prediction.formed',
199
+ 'executive.facet.progress',
200
+ 'plan.completed',
201
+ 'action.outcome', // 4.1: advance action-type goals when matching outcomes fire
202
+ ]
203
+ }
204
+ publishes(): CognitiveEventSchema[] {
205
+ return [
206
+ { type: 'goal.blocked', version: 1, validate: () => null },
207
+ { type: 'goal.achieved', version: 1, validate: () => null },
208
+ { type: 'goal.abandoned', version: 1, validate: () => null },
209
+ ]
210
+ }
211
+
212
+ onCognitiveEvent( e: CognitiveEvent ): StateCommands | void {
213
+ this._model.observe( e.type, e.salience )
214
+ switch( e.type ){
215
+ case 'energy.state.changed':
216
+ this._energyLevel = (e.payload as Record<string,number>)['level'] ?? this._energyLevel
217
+ break
218
+ case 'sleep.state.changed':
219
+ this._sleepPressure = (e.payload as Record<string,number>)['pressure'] ?? this._sleepPressure
220
+ break
221
+ case 'stress.state.changed':
222
+ this._stressLoad = (e.payload as Record<string,number>)['load'] ?? this._stressLoad
223
+ break
224
+ case 'belief.updated':
225
+ // Salience model only — authoritative count always read from state.metrics.
226
+ this._model.observe( 'belief.count', ( e.payload as Record<string,number> )['total'] ?? 0 )
227
+ break
228
+ case 'executive.goal.proposed':
229
+ // Track that executive proposed goals — prioritize executive-sourced goals slightly higher
230
+ this._executiveGoalConfidence = (e.payload as Record<string,number>)['confidence'] ?? 0.5
231
+ break
232
+ case 'executive.prediction.formed': {
233
+ const p = e.payload as { predictedDomains: string[]; confidence: number }
234
+ if (p.predictedDomains.includes('executive'))
235
+ this._model.setPrecision('goal.active_count', 1.0 + p.confidence * 0.5)
236
+ break
237
+ }
238
+ case 'executive.facet.progress': {
239
+ const payload = e.payload as {
240
+ facetId?: string
241
+ planId?: string
242
+ goalId?: string
243
+ goalProgress?: number
244
+ newGoals?: Array<{
245
+ description: string; priority: number; tags: string[]
246
+ completionType: string; completionCondition?: string
247
+ }>
248
+ goalsToAbandon?: Array<{ goalId: string; reason: string }>
249
+ newBeliefs?: Array<{
250
+ statement: string; category: string; confidence: number
251
+ evidence: 'single_observation' | 'recurring_pattern' | 'strong_pattern'; tags: string[]
252
+ }>
253
+ }
254
+
255
+ // Forward new goals to GoalManager
256
+ if( payload.newGoals )
257
+ for( const goal of payload.newGoals )
258
+ this.addGoal(
259
+ goal.description,
260
+ goal.priority,
261
+ goal.tags,
262
+ undefined,
263
+ undefined,
264
+ goal.completionType as 'action',
265
+ goal.completionCondition
266
+ )
267
+
268
+ // Forward goal abandonments
269
+ if( payload.goalsToAbandon )
270
+ for( const ga of payload.goalsToAbandon )
271
+ this.abandonGoal( ga.goalId, ga.reason )
272
+
273
+ if( payload.goalId && payload.goalProgress !== undefined ){
274
+ const goal = this._goals.get( payload.goalId )
275
+ if( goal?.status === 'active' ){
276
+ goal.progress = Math.max( goal.progress, payload.goalProgress ) // forward-only
277
+ if( goal.progress >= 0.95 )
278
+ goal.status = 'pending_verification'
279
+ }
280
+ }
281
+
282
+ break
283
+ }
284
+ case 'plan.completed': {
285
+ const
286
+ p = e.payload as { goalId: string },
287
+ goal = this._goals.get( p.goalId )
288
+
289
+ if( goal?.status === 'active' )
290
+ goal.status = 'pending_verification' // queued for condition eval next tick
291
+
292
+ break
293
+ }
294
+ case 'action.outcome': {
295
+ // 4.1: advance action-type goals when a real outcome matches their domain/tags.
296
+ // Epistemic/metric goals are advanced by their own mechanisms (_updateProgress).
297
+ const p = e.payload as { actionType: string; domain: string; outcomeQuality: number }
298
+ this._nudgeActionGoals( p.domain, p.actionType, p.outcomeQuality )
299
+ break
300
+ }
301
+ }
302
+ }
303
+
304
+ snapshot(): Record<string, unknown> {
305
+ return {
306
+ energyLevel: this._energyLevel,
307
+ sleepPressure: this._sleepPressure,
308
+ stressLoad: this._stressLoad,
309
+ }
310
+ }
311
+
312
+ async react(
313
+ _delta: Duration,
314
+ tick: Tick,
315
+ state: ReadonlySimulationState,
316
+ context: SimulationContext
317
+ ): Promise<EngineResult> {
318
+ const
319
+ events: Array<Omit<SimulationEvent, 'id' | 'timestamp' | 'tick'>> = [],
320
+ commands: StateCommands = { set: [], delete: [], metrics: [] }
321
+
322
+ // Refresh personality dispositions from the engine-config mirror (base ⊕
323
+ // persona-prior) so grit/persistence reflect both the PMA seed and metacognitive
324
+ // self-tuning over time — not hardcoded constants.
325
+ this._readConfigFromState( state )
326
+
327
+ // 0a. Snapshot current tick and belief count.
328
+ // Belief count is read directly from state.metrics — the only reliable source.
329
+ // The bus event (memory.state.changed) used to carry this but the payload field
330
+ // never matched; we now read the metric directly to avoid the stale-zero problem.
331
+ this._currentTick = tick
332
+ this._currentBeliefCount = state.metrics.get('memory.beliefs_total') ?? 0
333
+
334
+ // 0b. Sync goals seeded into state (e.g. bootstrap goals or LLM-generated goals)
335
+ this._syncFromStateGoals( state, tick )
336
+
337
+ // 1. Scan drives for new goal activation
338
+ this._activateFromDrives( state, tick )
339
+
340
+ // 2. Scan percepts for externally triggered goals
341
+ this._activateFromPercepts( state, tick )
342
+
343
+ // 3. Update priorities based on drive intensity and context
344
+ this._updatePriorities( state )
345
+
346
+ // 4. Check progress from state changes
347
+ this._updateProgress( state, tick )
348
+
349
+ // 5. Resolve goal conflicts (highest priority wins)
350
+ this._resolveConflicts()
351
+
352
+ // 6. Deactivate goals below threshold
353
+ this._deactivateStale( tick, state )
354
+
355
+ // 7. Persist goals as entities
356
+ this._persistGoals( commands, tick )
357
+
358
+ // 8. Metrics
359
+ const
360
+ activeGoals = Array.from( this._goals.values() ).filter( g => g.status === 'active'),
361
+ completedGoals = Array.from( this._goals.values() ).filter( g => g.status === 'completed')
362
+
363
+ const avgProgress = activeGoals.length > 0
364
+ ? activeGoals.reduce( ( s, g ) => s + g.progress, 0 ) / activeGoals.length
365
+ : 0
366
+
367
+ commands.metrics!.push(
368
+ [ 'goals.active', activeGoals.length ],
369
+ [ 'goals.completed_total', completedGoals.length ],
370
+ [ 'goals.total', this._goals.size ],
371
+ [ 'goals.top_priority', activeGoals[0]?.priority ?? 0 ],
372
+ [ 'goals.avg_progress', avgProgress ],
373
+ )
374
+
375
+ // Goal completion events — once per goal, on the completion transition.
376
+ // completedGoals accumulates across ticks; the _completedEmittedIds guard
377
+ // stops this re-firing every tick for every completed goal.
378
+ for( const goal of completedGoals )
379
+ if( goal.progress >= 1 && !this._completedEmittedIds.has( goal.id ) ){
380
+ this._completedEmittedIds.add( goal.id )
381
+ events.push({
382
+ type: 'goal.completed',
383
+ source: this.name,
384
+ payload: { goalId: goal.id, description: goal.description },
385
+ })
386
+ }
387
+
388
+ // Phase C: publish cognitive event
389
+ const _bus = this._bus
390
+ if( _bus && activeGoals.length >= 0 )
391
+ _bus.publish({
392
+ type: 'goal.state.changed',
393
+ version: 1,
394
+ sourceEngine: this.name,
395
+ salience: Math.max( 0.4, this._model.observe( 'goal.active_count', activeGoals.length ).salience ),
396
+ payload: { activeCount: activeGoals.length }
397
+ })
398
+
399
+ // Publish goal.achieved once per newly completed goal.
400
+ // Uses _achievedGoalIds to ensure the event fires exactly once per goal
401
+ // (completedGoals accumulates across ticks and would otherwise re-fire every tick).
402
+ if( _bus )
403
+ for( const goal of completedGoals )
404
+ if( !this._achievedGoalIds.has( goal.id ) ){
405
+ this._achievedGoalIds.add( goal.id )
406
+ const timeToComplete = tick - goal.activatedAt
407
+
408
+ _bus.publish({
409
+ type: 'goal.achieved', version: 1, sourceEngine: this.name,
410
+ salience: Math.min( 1, 0.5 + goal.priority * 0.5 ),
411
+ payload: { goalId: goal.id, description: goal.description, priority: goal.priority, timeToComplete },
412
+ })
413
+ }
414
+
415
+ // Detect newly-blocked goals and fire one event per goal on the transition tick
416
+ _bus && this._detectBlockedGoals( tick, activeGoals, _bus )
417
+
418
+ return { events: events.length > 0 ? events : undefined, commands }
419
+ }
420
+
421
+ private _detectBlockedGoals( tick: Tick, activeGoals: GoalState[], bus: CognitiveBus ): void {
422
+ for( const goal of activeGoals ){
423
+ const lastProgress = this._goalLastProgress.get( goal.id )
424
+
425
+ if( lastProgress !== undefined && Math.abs( goal.progress - lastProgress ) < 0.001 ){
426
+ // No progress change — increment stuck counter
427
+ if( !this._goalStuckSince.has( goal.id ) )
428
+ this._goalStuckSince.set( goal.id, tick )
429
+
430
+ const
431
+ stuckSince = this._goalStuckSince.get( goal.id )!,
432
+ ticksStuck = tick - stuckSince
433
+
434
+ // Fire on every _STUCK_THRESHOLD crossing so subscribers get
435
+ // escalating ticksStuck values (20, 40, 60...) and can detect resolution
436
+ // when events stop arriving.
437
+ if( ticksStuck > 0 && ticksStuck % this._STUCK_THRESHOLD === 0 )
438
+ bus.publish({
439
+ type: 'goal.blocked', version: 1, sourceEngine: this.name,
440
+ salience: Math.min( 1, 0.5 + goal.priority * 0.5 ),
441
+ payload: { goalId: goal.id, ticksStuck, priority: goal.priority },
442
+ })
443
+ }
444
+ // Progress moved — clear stuck state and log significant shifts
445
+ else {
446
+ this._goalStuckSince.delete( goal.id )
447
+ if( lastProgress !== undefined && Math.abs( goal.progress - lastProgress ) >= 0.05 ){
448
+ this._sessionLogger?.write({
449
+ type: 'goal.progress',
450
+ tick: tick as unknown as number,
451
+ goalId: goal.id,
452
+ description: goal.description,
453
+ previousProgress: lastProgress,
454
+ newProgress: goal.progress,
455
+ delta: goal.progress - lastProgress,
456
+ } as any)
457
+ }
458
+ }
459
+
460
+ this._goalLastProgress.set( goal.id, goal.progress )
461
+ }
462
+
463
+ // Clean up trackers for goals that are no longer active
464
+ for( const id of this._goalStuckSince.keys() )
465
+ if( !activeGoals.find( g => g.id === id ) ){
466
+ this._goalStuckSince.delete( id )
467
+ this._goalLastProgress.delete( id )
468
+ }
469
+ }
470
+
471
+ // ── Public API ───────────────────────────────────────────
472
+
473
+ /**
474
+ * Add a goal to the manager.
475
+ */
476
+ addGoal(
477
+ description: string,
478
+ basePriority: number,
479
+ tags: string[] = [],
480
+ parentGoalId?: string,
481
+ deadline?: Tick,
482
+ completionType: GoalState['completionType'] = 'epistemic',
483
+ completionCondition?: string,
484
+ id?: string,
485
+ requestingEntityId?: string,
486
+ requestingThreadId?: string,
487
+ ): string {
488
+ this._goalCounter++
489
+ const goalId = id ?? `goal-${this._goalCounter}`
490
+
491
+ this._goals.set( goalId, {
492
+ id: goalId,
493
+ description,
494
+ priority: basePriority,
495
+ basePriority,
496
+ progress: 0,
497
+ status: 'active',
498
+ parentGoalId,
499
+ subGoals: [],
500
+ activatedAt: this._currentTick,
501
+ deadline,
502
+ tags,
503
+ beliefsAtActivation: this._currentBeliefCount,
504
+ completionType,
505
+ completionCondition,
506
+ requestingEntityId,
507
+ requestingThreadId
508
+ })
509
+
510
+ if( parentGoalId ){
511
+ const parent = this._goals.get( parentGoalId )
512
+ if( parent ) parent.subGoals.push( goalId )
513
+ }
514
+
515
+ return goalId
516
+ }
517
+
518
+ /**
519
+ * Get all active goals sorted by priority.
520
+ */
521
+ getActiveGoals(): GoalState[] {
522
+ return Array.from( this._goals.values() )
523
+ .filter( g => g.status === 'active')
524
+ .sort( ( a, b ) => b.priority - a.priority )
525
+ }
526
+
527
+ getGoal( id: string ): GoalState | undefined {
528
+ return this._goals.get( id )
529
+ }
530
+
531
+ /**
532
+ * Mark a goal as completed.
533
+ */
534
+ completeGoal( goalId: string ): void {
535
+ const goal = this._goals.get( goalId )
536
+ if( goal ){
537
+ goal.status = 'completed'
538
+ goal.progress = 1
539
+
540
+ this._sessionLogger?.write({
541
+ type: 'goal.achieved',
542
+ tick: this._currentTick as unknown as number,
543
+ goalId: goal.id,
544
+ description: goal.description,
545
+ priority: goal.priority,
546
+ activatedAt: goal.activatedAt as unknown as number,
547
+ age: ( this._currentTick as unknown as number ) - ( goal.activatedAt as unknown as number ),
548
+ } as any)
549
+
550
+ // Cascade completion to parent
551
+ if( goal.parentGoalId ){
552
+ const parent = this._goals.get( goal.parentGoalId )
553
+ if( parent && parent.subGoals.every( sgId => this._goals.get( sgId )?.status === 'completed') ){
554
+ parent.progress = 1
555
+ parent.status = 'completed'
556
+ }
557
+ }
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Abandon a goal with an optional reason.
563
+ */
564
+ abandonGoal( goalId: string, reason?: string ): void {
565
+ const goal = this._goals.get( goalId )
566
+ if( goal && goal.status === 'active' ){
567
+ goal.status = 'abandoned'
568
+ // Optionally store the reason (if GoalState had a reason field; we can add it or store in tags)
569
+ reason && goal.tags.push(`abandoned:${reason.slice(0,50)}`)
570
+
571
+ this._sessionLogger?.write({
572
+ type: 'goal.abandoned',
573
+ tick: this._currentTick as unknown as number,
574
+ goalId: goal.id,
575
+ description: goal.description,
576
+ priority: goal.priority,
577
+ progress: goal.progress,
578
+ reason: reason?.slice( 0, 200 ),
579
+ age: ( this._currentTick as unknown as number ) - ( goal.activatedAt as unknown as number ),
580
+ } as any)
581
+
582
+ // Bus signal so the PlanningEngine can cancel any plans pursuing this goal
583
+ // (previously abandonment was session-log only — plans ran on regardless).
584
+ this._bus?.publish({
585
+ type: 'goal.abandoned', version: 1, sourceEngine: this.name,
586
+ salience: 0.55,
587
+ payload: { goalId: goal.id, reason: reason?.slice( 0, 200 ) },
588
+ })
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Update a goal's priority.
594
+ */
595
+ updateGoalPriority( goalId: string, newPriority: number ): void {
596
+ const goal = this._goals.get( goalId )
597
+ if( goal && goal.status === 'active')
598
+ goal.priority = Math.max( 0, Math.min( 1, newPriority ) )
599
+ }
600
+
601
+ // ── Internal: activation ─────────────────────────────────
602
+
603
+ private _syncFromStateGoals( state: ReadonlySimulationState, tick: Tick ): void {
604
+ for( const entity of state.entities.values() ){
605
+ if( entity.type !== 'goal') continue
606
+ if( this._goals.has( entity.id ) ) continue
607
+
608
+ const meta = entity.metadata ?? {}
609
+
610
+ const status = ( meta.status as GoalState['status'] ) ?? 'active'
611
+ if( status === 'completed' || status === 'abandoned') continue
612
+
613
+ this._goals.set( entity.id, {
614
+ id: entity.id,
615
+ description: ( meta.description as string ) ?? entity.id,
616
+ priority: ( meta.priority as number ) ?? 0.5,
617
+ basePriority: ( meta.basePriority as number ) ?? ( meta.priority as number ) ?? 0.5,
618
+ progress: ( meta.progress as number ) ?? 0,
619
+ status,
620
+ parentGoalId: meta.parentGoalId as string | undefined,
621
+ subGoals: ( meta.subGoals as string[] ) ?? [],
622
+ activatedAt: ( meta.activatedAt as Tick ) ?? tick,
623
+ deadline: meta.deadline as Tick | undefined,
624
+ tags: ( meta.tags as string[] ) ?? [],
625
+ beliefsAtActivation: ( meta.beliefsAtActivation as number ) ?? this._currentBeliefCount,
626
+ completionType: ( meta.completionType as GoalState['completionType'] ) ?? 'epistemic',
627
+ completionCondition: meta.completionCondition as string | undefined,
628
+ })
629
+ }
630
+ }
631
+
632
+ private _activateFromDrives( state: ReadonlySimulationState, _tick: Tick ): void {
633
+ const driveMappings: Array<{
634
+ drive: string; threshold: number
635
+ goalDesc: string; priority: number; tags: string[]
636
+ completionType?: GoalState['completionType'] // defaults to 'metric'
637
+ completionCondition?: string // metric goals only
638
+ }> = [
639
+ { drive: 'drive.energy', threshold: 0.4, goalDesc: 'Replenish energy', priority: 0.7, tags: [ 'survival', 'energy' ], completionCondition: 'energy.level > 70' },
640
+ { drive: 'drive.sleep', threshold: 0.5, goalDesc: 'Find rest opportunity', priority: 0.8, tags: [ 'survival', 'sleep' ], completionCondition: 'sleep.pressure < 20' },
641
+ { drive: 'drive.stress_reduction', threshold: 0.4, goalDesc: 'Reduce stress load', priority: 0.6, tags: [ 'wellbeing', 'stress' ], completionCondition: 'stress.load < 40' },
642
+ // `emotion.boredom` is a 0–1 metric (aesthetic.evaluator); the drive fires when it is
643
+ // sustained above 0.6, so the goal resolves once it falls back under 0.4 — a real
644
+ // hysteresis band, not the old `< 40` (always-true on a 0–1 scale → born-done loop).
645
+ { drive: 'drive.seek_engagement', threshold: 0.5, goalDesc: 'Seek stimulating engagement — reach out, explore, create, or learn something new to break the monotony', priority: 0.55, tags: [ 'engagement', 'boredom' ], completionCondition: 'emotion.boredom < 0.4' },
646
+ // Curiosity-to-resolve (Phase 3.a): a familiar-yet-unknown someone the Will keeps
647
+ // meeting generates an epistemic pull — "get to know them". Epistemic, so it resolves
648
+ // as the Will *learns* (belief formation, incl. keid-tagged beliefs from conversation),
649
+ // rather than against a metric threshold.
650
+ { drive: 'drive.curiosity_resolve', threshold: 0.4, goalDesc: 'Get to know the people I keep encountering but barely know', priority: 0.45, tags: [ 'curiosity', 'knowledge' ], completionType: 'epistemic' },
651
+ ]
652
+
653
+ for( const mapping of driveMappings ){
654
+ const driveIntensity = state.metrics.get( mapping.drive ) ?? 0
655
+ if( driveIntensity < mapping.threshold ) continue
656
+
657
+ const existing = Array.from( this._goals.values() )
658
+ .find( g => {
659
+ return g.tags.includes( mapping.tags[0]! )
660
+ && g.tags.includes( mapping.tags[1]! )
661
+ && g.status === 'active'
662
+ })
663
+
664
+ // Never spawn a goal that is *already satisfied* — it would complete on its
665
+ // creation tick and respawn the next, forever (a goal born done). Only create when
666
+ // there is genuine work toward the completion condition. (Guards a real drive/metric
667
+ // threshold mismatch found in a session log: seek-engagement spawned while
668
+ // `emotion.boredom < 40` was already true, producing one age-0 "achievement" per tick.)
669
+ if( !existing && !this._isConditionMet( mapping.completionCondition, state ) )
670
+ this.addGoal( mapping.goalDesc, mapping.priority, mapping.tags, undefined, undefined, mapping.completionType ?? 'metric', mapping.completionCondition )
671
+ }
672
+ }
673
+
674
+ private _activateFromPercepts( state: ReadonlySimulationState, _tick: Tick ): void {
675
+ for( const entity of state.entities.values() )
676
+ if( entity.type === 'attention.demand' && entity.metadata?.generatesGoal ){
677
+ const m = entity.metadata
678
+ const desc = ( m.goalDescription as string ) ?? 'Respond to demand'
679
+ // A demand may carry its own goal shape (tags / completion) — e.g. the per-entity
680
+ // curiosity pull from known.entity.tracker, an epistemic/metric goal keyed to one
681
+ // referent. Defaults preserve the original external-action-demand behaviour.
682
+ const tags = ( m.goalTags as string[] ) ?? [ 'external', 'demand' ]
683
+ const cType = ( m.goalCompletionType as GoalState['completionType'] ) ?? 'action'
684
+ const cCond = m.goalCompletionCondition as string | undefined
685
+ // Dedup by the referent (keid: tag) when present, else by description.
686
+ const keidTag = tags.find( t => t.startsWith( 'keid:' ) )
687
+ const existing = Array.from( this._goals.values() ).find( g =>
688
+ g.status === 'active' && ( keidTag ? g.tags.includes( keidTag ) : g.description === desc ) )
689
+
690
+ // Don't spawn a goal that's already satisfied (born-done guard — see _activateFromDrives).
691
+ if( !existing && !this._isConditionMet( cCond, state ) )
692
+ this.addGoal( desc, ( m.goalPriority as number ) ?? 0.5, tags, undefined, undefined, cType, cCond )
693
+ }
694
+ }
695
+
696
+ // ── Internal: priority ───────────────────────────────────
697
+
698
+ private _updatePriorities( state: ReadonlySimulationState ): void {
699
+ // Task-persistence: which goal the Will is focused on + how committed (task.switcher)
700
+ // and the (developable) cost of switching away. Drives the commitment boost below.
701
+ const focusMeta = state.entities.get( 'task-switch-focus' )?.metadata as Record<string, unknown> | undefined
702
+ const focusedGoalId = focusMeta?.goalId as string | undefined
703
+ const focusTicks = ( focusMeta?.focusTicks as number ) ?? 0
704
+ const switchCost = state.metrics.get( 'task_switch.switch_cost' ) ?? 0
705
+
706
+ for( const goal of this._goals.values() ){
707
+ if( goal.status !== 'active') continue
708
+
709
+ // Commitment boost — only the focused goal, bounded, never accumulating. Builds with
710
+ // focus duration, amplified by sunk cost in an in-progress plan, scaled by switch cost.
711
+ let commitmentBoost = 0
712
+ if( goal.id === focusedGoalId && focusTicks > 0 && switchCost > 0 ){
713
+ const commitment = Math.min( 1, focusTicks / FOCUS_COMMITMENT_RAMP )
714
+ const planProgress = this._focusedPlanProgress( state, goal.id )
715
+ commitmentBoost = Math.min(
716
+ MAX_COMMITMENT_BOOST,
717
+ switchCost * commitment * ( 1 + planProgress ) * COMMITMENT_GAIN
718
+ )
719
+ }
720
+
721
+ // Drive-aligned goals get priority boost from drive intensity
722
+ let driveBoost = 0
723
+ if( goal.tags.includes('energy') )
724
+ driveBoost += ( state.metrics.get('drive.energy') ?? 0 ) * 0.3
725
+
726
+ if( goal.tags.includes('sleep') )
727
+ driveBoost += ( state.metrics.get('drive.sleep') ?? 0 ) * 0.3
728
+
729
+ if( goal.tags.includes('engagement') )
730
+ driveBoost += ( state.metrics.get('drive.seek_engagement') ?? 0 ) * 0.25
731
+
732
+ // Deadline pressure: boost before deadline, decay faster after
733
+ let deadlineBoost = 0
734
+ if( goal.deadline ){
735
+ const ticksUntilDeadline = ( goal.deadline as unknown as number ) - ( this._currentTick as unknown as number )
736
+ if( ticksUntilDeadline > 0 ){
737
+ // Urgency ramps linearly in the last 50 ticks before deadline
738
+ const urgency = Math.max( 0, 1 - ticksUntilDeadline / 50 )
739
+ deadlineBoost = urgency * 0.4
740
+ } else {
741
+ // Overdue: decay priority 3× faster — stale goals should yield to fresh ones
742
+ deadlineBoost = -this._priorityDecayRate * 2
743
+ }
744
+ }
745
+
746
+ // Decay priority toward base
747
+ goal.priority = goal.basePriority
748
+ + driveBoost
749
+ + deadlineBoost
750
+ + commitmentBoost
751
+ - this._priorityDecayRate
752
+
753
+ goal.priority = Math.max( 0, Math.min( 1, goal.priority ) )
754
+ }
755
+ }
756
+
757
+ /**
758
+ * Plan completion (0..1) for a goal, read from the live `plan` entity (planning.engine).
759
+ * Sunk cost: a goal with a half-finished plan is costlier to abandon, so it amplifies the
760
+ * task-persistence commitment boost. 0 when the goal has no plan or an empty one.
761
+ */
762
+ private _focusedPlanProgress( state: ReadonlySimulationState, goalId: string ): number {
763
+ for( const e of state.entities.values() ){
764
+ if( e.type !== 'plan' || e.metadata?.goalId !== goalId ) continue
765
+ const steps = ( e.metadata?.steps as Array<{ status?: string }> | undefined ) ?? []
766
+ if( steps.length === 0 ) continue
767
+ const done = steps.filter( s => s.status === 'completed' || s.status === 'skipped' ).length
768
+ return done / steps.length
769
+ }
770
+ return 0
771
+ }
772
+
773
+ private _resolveConflicts(): void {
774
+ const active = this.getActiveGoals()
775
+
776
+ // Deactivate lowest-priority goals if over capacity
777
+ while( active.length > this._maxActiveGoals ){
778
+ const lowest = active.pop()
779
+ if( lowest ) lowest.status = 'pending'
780
+ }
781
+ }
782
+
783
+ // ── Internal: progress ───────────────────────────────────
784
+
785
+ private _updateProgress( state: ReadonlySimulationState, tick: Tick ): void {
786
+ // Read from state.metrics — authoritative, updated every tick by semantic integrator.
787
+ const currentBeliefs = state.metrics.get('memory.beliefs_total') ?? 0
788
+
789
+ for( const goal of this._goals.values() ){
790
+ // A goal cannot be born and die in the same tick. Give it at least one tick to be
791
+ // worked on before it can complete — this kills age-0 "achievements" universally
792
+ // (defence-in-depth beyond the already-satisfied creation guard in _activateFromDrives).
793
+ if( goal.activatedAt === tick ) continue
794
+
795
+ if( goal.status === 'pending_verification' ){
796
+ const met = this._evaluateMetricProgress( goal, state ) >= 1
797
+ met ? this.completeGoal( goal.id ) : ( goal.status = 'active' )
798
+
799
+ continue // skip completionType switch entirely
800
+ }
801
+
802
+ if( goal.status !== 'active' && goal.status !== 'blocked' ) continue
803
+
804
+ switch( goal.completionType ){
805
+ case 'metric':
806
+ goal.progress = this._evaluateMetricProgress( goal, state )
807
+ break
808
+
809
+ case 'action':
810
+ // Progress is advanced externally via onCognitiveEvent('action.outcome')
811
+ // → _nudgeActionGoals(). _updateProgress() only checks for completion here.
812
+ // _deactivateStale abandons it if stuck for too long under frustration.
813
+ break
814
+
815
+ case 'epistemic':
816
+ default: {
817
+ const newBeliefs = currentBeliefs - goal.beliefsAtActivation
818
+ goal.progress = Math.min( 1, newBeliefs / this._epistemicBeliefThreshold )
819
+ break
820
+ }
821
+ }
822
+
823
+ goal.progress >= 1 && this.completeGoal( goal.id )
824
+ }
825
+ }
826
+
827
+ /**
828
+ * Evaluate progress for a metric goal.
829
+ *
830
+ * If the goal has a completionCondition (e.g. "stress.load < 40"), parse and
831
+ * compute smooth 0-1 progress toward it. Falls back to tag-based heuristics
832
+ * for the three built-in drive metrics when no condition is specified.
833
+ */
834
+
835
+ /**
836
+ * 4.1: Nudge progress on `action`-type goals when an action.outcome event fires
837
+ * whose domain or actionType overlaps with the goal's tags.
838
+ *
839
+ * Progress is incremented by `outcomeQuality × 0.12` per matched action, so
840
+ * a goal with a single matching tag needs ~9 successful actions (at full quality)
841
+ * to complete — a realistic bar for discrete, real-world tasks.
842
+ *
843
+ * Substring matching (both directions) handles common mismatches between
844
+ * effector names and goal tags (e.g. "communicate" ↔ "communication",
845
+ * "learn" ↔ "learning").
846
+ */
847
+ private _nudgeActionGoals(
848
+ domain: string,
849
+ actionType: string,
850
+ outcomeQuality: number
851
+ ): void {
852
+ const dLow = domain.toLowerCase()
853
+ const aLow = actionType.toLowerCase()
854
+
855
+ // Communication actions — 'talk' and 'text' are always treated as
856
+ // matching the 'communication' and 'reply' tag families so goals created
857
+ // in response to incoming messages get their progress nudged.
858
+ const isCommunicationAction = aLow === 'talk' || aLow === 'text'
859
+ || dLow === 'communication'
860
+
861
+ for( const goal of this._goals.values() ){
862
+ if( goal.status !== 'active' && goal.status !== 'blocked' ) continue
863
+ if( goal.completionType !== 'action' ) continue
864
+
865
+ const hasMatch = goal.tags.some( tag => {
866
+ const t = tag.toLowerCase()
867
+ return t === dLow
868
+ || t === aLow
869
+ || dLow.includes( t ) || t.includes( dLow )
870
+ || aLow.includes( t ) || t.includes( aLow )
871
+ }) || ( isCommunicationAction && goal.tags.some( t =>
872
+ t === 'communication' || t === 'reply' || t === 'conversation'
873
+ ) )
874
+
875
+ if( hasMatch ){
876
+ goal.progress = Math.min( 1, goal.progress + outcomeQuality * 0.12 )
877
+ goal.lastActionAttemptTick = this._currentTick
878
+ goal.lastActionType = actionType
879
+ }
880
+ }
881
+ }
882
+
883
+ /** True when a metric completionCondition (e.g. "emotion.boredom < 40") is already met. */
884
+ private _isConditionMet( condition: string | undefined, state: ReadonlySimulationState ): boolean {
885
+ if( !condition ) return false
886
+ const m = condition.match( /^([\w.]+)\s*([<>]=?)\s*([\d.]+)$/ )
887
+ if( !m ) return false
888
+ const [ , key, op, raw ] = m
889
+ const current = state.metrics.get( key! ) ?? 0
890
+ const threshold = parseFloat( raw! )
891
+ return op === '<' ? current < threshold
892
+ : op === '<=' ? current <= threshold
893
+ : op === '>' ? current > threshold
894
+ : current >= threshold
895
+ }
896
+
897
+ private _evaluateMetricProgress(
898
+ goal: GoalState,
899
+ state: ReadonlySimulationState
900
+ ): number {
901
+ if( goal.completionCondition ){
902
+ const m = goal.completionCondition.match( /^([\w.]+)\s*([<>]=?)\s*([\d.]+)$/ )
903
+ if( m ){
904
+ const [ , key, op, rawThreshold ] = m
905
+ const current = state.metrics.get( key! ) ?? 0
906
+ const threshold = parseFloat( rawThreshold! )
907
+
908
+ const met = op === '<' ? current < threshold
909
+ : op === '<=' ? current <= threshold
910
+ : op === '>' ? current > threshold
911
+ : current >= threshold
912
+
913
+ if( met ) return 1
914
+
915
+ // Smooth gradient toward the threshold. Infer the metric's range from the threshold:
916
+ // ≤ 1 ⇒ a 0–1 metric (e.g. emotion.boredom), otherwise the 0–100 default. Clamped.
917
+ const scale = threshold <= 1 ? 1 : 100
918
+ return op === '<' || op === '<='
919
+ ? Math.max( 0, Math.min( 1, ( scale - current ) / ( scale - threshold ) ) )
920
+ : Math.max( 0, Math.min( 1, current / threshold ) )
921
+ }
922
+ }
923
+
924
+ // Tag-based fallback for the three built-in drive metrics
925
+ if( goal.tags.includes('energy') )
926
+ return Math.min( 1, this._energyLevel / 100 )
927
+
928
+ if( goal.tags.includes('sleep') )
929
+ return Math.max( 0, 1 - ( this._sleepPressure ) / 100 )
930
+
931
+ if( goal.tags.includes('stress') )
932
+ return Math.max( 0, 1 - this._stressLoad / 100 )
933
+
934
+ return goal.progress // no update if no condition and no known tag
935
+ }
936
+
937
+ /**
938
+ * Refresh personality-derived dispositions from `engine-config-goal-manager`
939
+ * (base params ⊕ persona-prior deltas). Grit/persistence is a per-Will trait
940
+ * seeded by the PMA and developed by the metacognition cycle — not a constant.
941
+ */
942
+ private _readConfigFromState( state: ReadonlySimulationState ): void {
943
+ const p = readEffectiveParams( state, 'engine-config-goal-manager' )
944
+ if( p.gritPriority != null ) this._gritPriority = p.gritPriority
945
+ if( p.gritPatienceScale != null ) this._gritPatienceScale = p.gritPatienceScale
946
+ if( p.frustrationTolerance != null ) this._frustrationTolerance = p.frustrationTolerance
947
+ }
948
+
949
+ private _deactivateStale( tick: Tick, state: ReadonlySimulationState ): void {
950
+ // Frustration compresses the patience window — but frustrationTolerance (a
951
+ // personality trait) dampens that, so a resilient mind doesn't give up faster
952
+ // just because it's frustrated.
953
+ const tol = Math.max( 0, Math.min( 1, this._frustrationTolerance ) )
954
+ const frustration = ( state.metrics.get('emotion.frustration') ?? 0 ) * ( 1 - tol )
955
+ const patienceTicks = frustration > 0.6
956
+ ? 80 // gave up quickly under high (tolerance-adjusted) frustration
957
+ : frustration > 0.3
958
+ ? 130 // moderate impatience
959
+ : 200 // default — full patience window
960
+
961
+ for( const goal of this._goals.values() ){
962
+ if( goal.status !== 'active') continue
963
+ if( goal.priority < this._deactivationThreshold ){
964
+ goal.status = 'abandoned'
965
+ continue
966
+ }
967
+
968
+ // Resilience / grit: importance buys persistence. A sufficiently important goal
969
+ // is NEVER auto-abandoned by staleness — the mind only lets it go by a deliberate
970
+ // executive decision (goalsToAbandon). What matters isn't dropped by a timer.
971
+ if( goal.priority >= this._gritPriority ) continue
972
+
973
+ // Below the grit threshold, patience scales with priority — a more-important
974
+ // stuck goal is pursued much longer before the mind gives up on it.
975
+ const scaledPatience = patienceTicks * ( 1 + goal.priority * this._gritPatienceScale )
976
+ const age = tick - goal.activatedAt
977
+ if( age > scaledPatience && goal.progress < 0.1 )
978
+ goal.status = 'abandoned'
979
+ }
980
+ }
981
+
982
+ // ── Internal: persistence ────────────────────────────────
983
+
984
+ private _persistGoals( commands: StateCommands, tick: Tick ): void {
985
+ for( const goal of this._goals.values() )
986
+ commands.set!.push({
987
+ id: goal.id,
988
+ type: 'goal',
989
+ updatedAt: tick,
990
+ metadata: {
991
+ description: goal.description,
992
+ priority: goal.priority,
993
+ basePriority: goal.basePriority,
994
+ progress: goal.progress,
995
+ status: goal.status,
996
+ parentGoalId: goal.parentGoalId,
997
+ subGoals: goal.subGoals,
998
+ deadline: goal.deadline,
999
+ tags: goal.tags,
1000
+ beliefsAtActivation: goal.beliefsAtActivation,
1001
+ completionType: goal.completionType,
1002
+ completionCondition: goal.completionCondition,
1003
+ activatedAt: goal.activatedAt,
1004
+ tick,
1005
+ },
1006
+ })
1007
+ }
1008
+ }