@mindot/will 0.4.0 → 0.6.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 (135) hide show
  1. package/README.md +32 -12
  2. package/dist/channels/discord.d.ts +2 -11
  3. package/dist/channels/discord.js.map +1 -1
  4. package/dist/channels/whatsapp.d.ts +72 -0
  5. package/dist/channels/whatsapp.js +252 -0
  6. package/dist/channels/whatsapp.js.map +1 -0
  7. package/dist/cli.js +837 -91
  8. package/dist/cli.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +620 -79
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp/effectors.d.ts +1 -1
  13. package/dist/mcp/effectors.js.map +1 -1
  14. package/dist/types-E9-HV-SW.d.ts +11 -0
  15. package/dist/{will-D-slky1N.d.ts → will-Bikuk4s2.d.ts} +56 -12
  16. package/package.json +12 -3
  17. package/src/channels/discord.ts +11 -11
  18. package/src/channels/roster.ts +1 -1
  19. package/src/channels/types.ts +1 -1
  20. package/src/channels/whatsapp.ts +318 -0
  21. package/src/cli.ts +46 -23
  22. package/src/cognition/acp.ts +28 -0
  23. package/src/cognition/agency/consequence.ts +174 -0
  24. package/src/cognition/agency/engines/action.selector.ts +301 -48
  25. package/src/cognition/agency/engines/affordance.synthesizer.ts +26 -16
  26. package/src/cognition/agency/engines/deliberation.engine.ts +28 -14
  27. package/src/cognition/agency/engines/motor.schema.executor.ts +86 -14
  28. package/src/cognition/agency/engines/reafference.engine.ts +122 -18
  29. package/src/cognition/agency/execution.primitives.ts +13 -13
  30. package/src/cognition/agency/proactive.communicator.ts +1 -1
  31. package/src/cognition/agency/reconcile.learning.ts +1 -1
  32. package/src/cognition/agency/revocation.ts +79 -0
  33. package/src/cognition/agency/schemas/repertoire.ts +2 -2
  34. package/src/cognition/agency/selection.scoring.ts +25 -9
  35. package/src/cognition/bus.ts +2 -2
  36. package/src/cognition/completion.inbox.ts +1 -1
  37. package/src/cognition/config.mirror.entities.ts +5 -5
  38. package/src/cognition/event.log.ts +2 -2
  39. package/src/cognition/event.schemas.ts +67 -67
  40. package/src/cognition/faculties/aesthetic.evaluator.ts +6 -6
  41. package/src/cognition/faculties/affective.blender.ts +36 -4
  42. package/src/cognition/faculties/attachment.evaluator.ts +6 -6
  43. package/src/cognition/faculties/attention.allocator.ts +63 -8
  44. package/src/cognition/faculties/autobiographical.narrator.ts +3 -3
  45. package/src/cognition/faculties/bias.detector.ts +7 -7
  46. package/src/cognition/faculties/circadian.oscillator.ts +3 -3
  47. package/src/cognition/faculties/confidence.calibrator.ts +7 -7
  48. package/src/cognition/faculties/dream.simulator.ts +5 -5
  49. package/src/cognition/faculties/empathy.simulator.ts +7 -7
  50. package/src/cognition/faculties/energy.regulator.ts +4 -4
  51. package/src/cognition/faculties/episodic.consolidator.ts +6 -6
  52. package/src/cognition/faculties/executive.engine/commands.ts +6 -6
  53. package/src/cognition/faculties/executive.engine/context.ts +42 -42
  54. package/src/cognition/faculties/executive.engine/deferred.effects.ts +2 -2
  55. package/src/cognition/faculties/executive.engine/engine.ts +27 -26
  56. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +12 -12
  57. package/src/cognition/faculties/executive.engine/facet.ts +18 -18
  58. package/src/cognition/faculties/executive.engine/gating.ts +2 -2
  59. package/src/cognition/faculties/executive.engine/messages.ts +3 -3
  60. package/src/cognition/faculties/executive.engine/parser.ts +11 -11
  61. package/src/cognition/faculties/executive.engine/prompt.factory.ts +55 -55
  62. package/src/cognition/faculties/exteroception.ts +99 -5
  63. package/src/cognition/faculties/forgetting.curve.ts +3 -3
  64. package/src/cognition/faculties/frustration.evaluator.ts +6 -6
  65. package/src/cognition/faculties/goal.manager.ts +15 -15
  66. package/src/cognition/faculties/inhibition.controller.ts +3 -3
  67. package/src/cognition/faculties/interoception.ts +3 -3
  68. package/src/cognition/faculties/introspection.engine.ts +3 -3
  69. package/src/cognition/faculties/known.entity.tracker.ts +15 -15
  70. package/src/cognition/faculties/loss.evaluator.ts +2 -2
  71. package/src/cognition/faculties/moral.evaluator.ts +4 -4
  72. package/src/cognition/faculties/novelty.detector.ts +4 -4
  73. package/src/cognition/faculties/persona.consolidator.ts +12 -12
  74. package/src/cognition/faculties/planning.engine/engine.ts +47 -47
  75. package/src/cognition/faculties/planning.engine/plan.frontier.ts +5 -5
  76. package/src/cognition/faculties/planning.engine/plan.store.ts +1 -1
  77. package/src/cognition/faculties/planning.engine/plan.supervision.ts +20 -20
  78. package/src/cognition/faculties/reputation.tracker.ts +6 -6
  79. package/src/cognition/faculties/reward.evaluator.ts +4 -4
  80. package/src/cognition/faculties/self.model.updater.ts +8 -8
  81. package/src/cognition/faculties/semantic.engine/integrator.ts +15 -15
  82. package/src/cognition/faculties/sleep.pressure.regulator.ts +4 -4
  83. package/src/cognition/faculties/social.perception.ts +3 -3
  84. package/src/cognition/faculties/spaced.repetition.ts +2 -2
  85. package/src/cognition/faculties/stress.regulator.ts +27 -4
  86. package/src/cognition/faculties/task.switcher.ts +11 -6
  87. package/src/cognition/faculties/theory.of.mind.ts +6 -6
  88. package/src/cognition/faculties/threat.evaluator.ts +3 -3
  89. package/src/cognition/faculties/working.memory.ts +8 -8
  90. package/src/cognition/generative.model.ts +2 -2
  91. package/src/cognition/memory/vector.adapter.ts +5 -5
  92. package/src/cognition/memory/vector.content.ts +4 -4
  93. package/src/cognition/memory/vector.embedder.ts +9 -9
  94. package/src/cognition/persona.prior.ts +1 -1
  95. package/src/cognition/schema.registry.ts +2 -2
  96. package/src/cognition/senses/audition.engine/engine.ts +2 -2
  97. package/src/cognition/senses/base.sense.engine.ts +1 -1
  98. package/src/cognition/utilities/token.tracker.ts +11 -5
  99. package/src/core/abstracts.ts +1 -1
  100. package/src/core/completion.recorder.ts +2 -2
  101. package/src/core/serialization.ts +3 -3
  102. package/src/core/snapshot.manager.ts +1 -1
  103. package/src/core/state.manager.ts +1 -1
  104. package/src/core/utils.ts +1 -1
  105. package/src/host/boot.ts +59 -37
  106. package/src/host/utterances.ts +1 -1
  107. package/src/llm/index.ts +84 -34
  108. package/src/llm/summarizer.ts +4 -4
  109. package/src/llm/wire.contracts.ts +1 -1
  110. package/src/mcp/effectors.ts +10 -10
  111. package/src/mcp/server.ts +11 -11
  112. package/src/pma/eval.ts +1 -1
  113. package/src/pma/index.ts +21 -21
  114. package/src/runners/outreach.runner.ts +3 -3
  115. package/src/runners/social.runner.ts +1 -1
  116. package/src/runners/thin-shim.runner.ts +1 -1
  117. package/src/sdk/will.ts +20 -11
  118. package/src/serve/server.ts +12 -12
  119. package/src/stem/assembly.audit.ts +3 -3
  120. package/src/stem/guards/identity.coherence.ts +13 -12
  121. package/src/stem/guards/identity.guard.ts +22 -22
  122. package/src/stem/index.ts +11 -11
  123. package/src/stem/mind.ts +12 -12
  124. package/src/stem/tracts/biography.writer.ts +3 -3
  125. package/src/stem/tracts/effector.controller.ts +4 -4
  126. package/src/stem/tracts/health.reporter.ts +1 -1
  127. package/src/stem/tracts/outbox.controller.ts +4 -0
  128. package/src/stem/tracts/outbox.writer.ts +4 -4
  129. package/src/stem/tracts/pma.controller.ts +1 -1
  130. package/src/stem/tracts/replay.controller.ts +10 -10
  131. package/src/stem/tracts/sensory.controller.ts +2 -2
  132. package/src/stem/tracts/session.logger.ts +4 -4
  133. package/src/stem/tracts/transport/socketio.transport.ts +7 -7
  134. package/src/stem/tracts/transport/stream.transport.ts +2 -2
  135. package/src/stem/tracts/transport.controller.ts +15 -15
@@ -5,7 +5,8 @@
5
5
  // The ActionSelector runs every tick with no LLM. It reads the affordance field
6
6
  // the synthesizer wrote, assembles the bias signals the affective / drive /
7
7
  // inhibition engines already expose, and runs a soft winner-take-all (see
8
- // scoring.ts). The winner is committed as an `agency.intent` for the executor.
8
+ // selection.scoring.ts). The winner is committed as an `agency.intent` for
9
+ // the executor.
9
10
  //
10
11
  // System 1 is the default: the substrate selects and commits on its own. The
11
12
  // executive (System 2, the LLM) is recruited only when the choice is genuinely
@@ -25,7 +26,7 @@ import type {
25
26
  Duration, Tick, SimulationContext,
26
27
  ReadonlySimulationState, StateCommands, EntityInput,
27
28
  } from '#core/types'
28
- import type { CognitiveBus } from '#cognition/bus'
29
+ import type { CognitiveBus, CognitiveEvent } from '#cognition/bus'
29
30
  import type { CognitiveEngine, EngineResult } from '#cognition/types'
30
31
  import type { CognitiveEventSchema } from '#cognition/schema.registry'
31
32
  import type { Affordance, AffordanceSource, ScoredAffordance } from '#agency/types'
@@ -35,6 +36,8 @@ import {
35
36
  type BiasContext, type ScoreWeights,
36
37
  } from '#agency/selection.scoring'
37
38
  import { readEffectiveParams, readPersonaPrior } from '#cognition/persona.prior'
39
+ import { RUPTURE_REVOKE_GATE, revocationEntity } from '#agency/revocation'
40
+ import { liveConsequences, matchConsequenceText } from '#agency/consequence'
38
41
 
39
42
  /**
40
43
  * Activation margin (winner − runner-up) below which the choice is "contested" —
@@ -94,12 +97,44 @@ const AWAIT_STALE_TICKS = 15
94
97
  /** How much a fully-stale awaiting incumbent's strength decays (0..1). */
95
98
  const STALE_DECAY = 0.5
96
99
 
100
+ // ── Exafferent rupture (EXAFFERENCE P3) ────────────────────────
101
+ /**
102
+ * The world can revoke engagement, not only win it. `rupture` is a scalar in
103
+ * [0,1] read pull-style from the exafferent percepts P2 tagged: when the world
104
+ * moves on its own strongly enough to seize attention, it softens the Will's
105
+ * commitment to what it was doing — "let go of waiting". Gate = WORKSPACE_THRESHOLD
106
+ * so rupture ≈ "this percept would seize the global workspace". Because only
107
+ * `provenance:'exafferent'` percepts count, the mind can never be ruptured by the
108
+ * echo of its own action (P2 tagged those `reafferent` and attenuated them).
109
+ */
110
+ const RUPTURE_SALIENCE_GATE = 0.4
111
+ /** Count exafferent percepts this fresh — matches the percept lifespan
112
+ * (Exteroception cleans up percepts older than 2 ticks), so a shock sustains
113
+ * rupture for exactly as long as its percept is alive, then decays. */
114
+ const RUPTURE_WINDOW_TICKS = 2
115
+ /** Per-tick mean-reversion of `situation.stability` back toward 1 (calm). */
116
+ const STABILITY_RECOVERY = 0.05
117
+ /** Snap-to-1 threshold: a settled mind stops re-writing the stability metric,
118
+ * so a never-ruptured run is byte-identical to pre-P3 (the metric is absent). */
119
+ const STABILITY_EPSILON = 1e-4
120
+ /** How many ticks a revocation stays "recent" enough to flavor the next
121
+ * deliberation formed in its wake (Channel-B hint); after this it's forgotten. */
122
+ const REVOKE_HINT_WINDOW = 8
123
+
97
124
  export class ActionSelector implements CognitiveEngine {
98
125
  readonly name = 'action-selector'
99
126
 
100
127
  private _bus: CognitiveBus | null = null
101
128
  private _lastEntropy = 0
102
129
  private _lastDeliberate = false
130
+ // EXAFFERENCE P4 follow-up (Channel B): the schema we most recently revoked
131
+ // under rupture, so the *next* deliberation formed in its wake can own the
132
+ // interruption in-character ("something changed — I dropped what I was
133
+ // weighing"). Telemetry-grade instance state, mirroring _lastEntropy.
134
+ private _lastRevoked: { schema: string; tick: number } | null = null
135
+ // ACP §2b: sense-channel percepts buffered off the bus for the next react's
136
+ // rupture computation (they never become entities). Cross-tick ⇒ FN9.
137
+ private _senseBuffer: Array<{ salience: number; text?: string }> = []
103
138
 
104
139
  attachBus( bus: CognitiveBus ): void { this._bus = bus }
105
140
 
@@ -108,12 +143,62 @@ export class ActionSelector implements CognitiveEngine {
108
143
  { type: 'agency.selection.made', version: 1, validate: () => null },
109
144
  { type: 'agency.selection.ambiguous', version: 1, validate: () => null },
110
145
  { type: 'agency.action.preempted', version: 1, validate: () => null },
146
+ { type: 'agency.situation.rupture', version: 1, validate: () => null },
147
+ { type: 'agency.commitment.revoked', version: 1, validate: () => null },
148
+ ]
149
+ }
150
+ subscribes(): string[] {
151
+ return [
152
+ 'senses.*',
153
+ // Registry #6 (ACP-P3): the model-error term — each carries its
154
+ // engine's prediction-error salience, ALREADY precision-attenuated for
155
+ // self-caused swings by the ACP-P2 consumers, so the echo guard
156
+ // composes by construction: our own action's interoceptive wake
157
+ // arrives below the rupture gate.
158
+ 'attention.state.changed', 'affect.state.changed', 'stress.state.changed',
111
159
  ]
112
160
  }
113
- subscribes(): string[] { return [] }
114
- onCognitiveEvent(): void { /* pull model — reads the field from frozen state */ }
161
+ /**
162
+ * Mostly pull-model — but two afferent classes never become entities and
163
+ * would leave rupture blind: sense-channel percepts
164
+ * (ACTION_CONDITIONED_PREDICTION §2b) and the model-error state-change
165
+ * events (registry #6). Buffer both (cross-tick: bus flush at T, consumed
166
+ * by react at T+1 — FN9-snapshotted); the echo guard is applied at read
167
+ * time for texts, and at the SOURCE for model errors (ACP-P2 precision).
168
+ */
169
+ onCognitiveEvent( e: CognitiveEvent ): void {
170
+ const isSense = e.type.startsWith('senses.') && e.type.endsWith('.percept')
171
+ const isModelError = e.type === 'attention.state.changed'
172
+ || e.type === 'affect.state.changed' || e.type === 'stress.state.changed'
173
+ if( !isSense && !isModelError ) return
174
+ const p = e.payload as { content?: unknown } | undefined
175
+ this._senseBuffer.push({
176
+ salience: e.salience,
177
+ ...( isSense && typeof p?.content === 'string' ? { text: p.content } : {} ),
178
+ })
179
+ }
115
180
  snapshot(): Record<string, unknown> {
116
- return { lastEntropy: this._lastEntropy, lastDeliberate: this._lastDeliberate }
181
+ return {
182
+ lastEntropy: this._lastEntropy, lastDeliberate: this._lastDeliberate,
183
+ lastRevoked: this._lastRevoked, senseBuffer: this._senseBuffer,
184
+ }
185
+ }
186
+ /** FN9: `_lastRevoked` has behavioral effect (the Channel-B `revokedBy` stamp),
187
+ * so a restored mind must carry it — a rupture-driven letting-go survives a
188
+ * snapshot boundary instead of silently losing its narrative thread. */
189
+ restore( s: Record<string, unknown> ): void {
190
+ if( typeof s['lastEntropy'] === 'number' ) this._lastEntropy = s['lastEntropy']
191
+ if( typeof s['lastDeliberate'] === 'boolean' ) this._lastDeliberate = s['lastDeliberate']
192
+ const lr = s['lastRevoked'] as { schema?: unknown; tick?: unknown } | null | undefined
193
+ this._lastRevoked = lr && typeof lr === 'object' && typeof lr.schema === 'string' && typeof lr.tick === 'number'
194
+ ? { schema: lr.schema, tick: lr.tick }
195
+ : null
196
+ const sb = s['senseBuffer']
197
+ this._senseBuffer = Array.isArray( sb )
198
+ ? ( sb as Array<{ salience?: unknown; text?: unknown }> )
199
+ .filter( it => it && typeof it.salience === 'number' )
200
+ .map( it => ({ salience: it.salience as number, ...( typeof it.text === 'string' ? { text: it.text } : {} ) }) )
201
+ : []
117
202
  }
118
203
 
119
204
  async react(
@@ -131,11 +216,14 @@ export class ActionSelector implements CognitiveEngine {
131
216
  const eligible: Affordance[] = []
132
217
  const intents: Array<{ id: string; st: string; parentIntentId?: string; activation: number; schema: string; target: string; dispatchedAt: number }> = []
133
218
  const expandingParents = new Set<string>()
219
+
134
220
  for( const [ id, e ] of state.entities ){
135
- if( e.type === 'agency.intent' ){
221
+ if( e.type === 'agency.intent'){
136
222
  const m = ( e.metadata ?? {} ) as Record<string, unknown>
137
223
  const st = str( m['status'] ) ?? ''
138
- if( st === 'expanding' ) expandingParents.add( id )
224
+
225
+ if( st === 'expanding') expandingParents.add( id )
226
+
139
227
  intents.push({
140
228
  id, st,
141
229
  parentIntentId: str( m['parentIntentId'] ),
@@ -144,9 +232,12 @@ export class ActionSelector implements CognitiveEngine {
144
232
  target: str( m['targetEntityId'] ) ?? '',
145
233
  dispatchedAt: num( m['dispatchedAt'], tick ),
146
234
  })
235
+
147
236
  continue
148
237
  }
149
- if( e.type !== 'affordance' ) continue
238
+
239
+ if( e.type !== 'affordance') continue
240
+
150
241
  const a = readAffordance( id, e.metadata )
151
242
  if( a.available ) eligible.push( a )
152
243
  }
@@ -157,25 +248,96 @@ export class ActionSelector implements CognitiveEngine {
157
248
  let blocking = false
158
249
  let awaiting: { id: string; activation: number; schema: string; target: string; dispatchedAt: number } | null = null
159
250
  let composite: { id: string; activation: number; schema: string } | null = null
251
+ let deliberating: { id: string; schema: string } | null = null
252
+
160
253
  for( const it of intents ){
161
- if( it.st === 'deliberating' ) blocking = true
162
- else if( it.st === 'expanding' ) composite = { id: it.id, activation: it.activation, schema: it.schema }
163
- else if( it.st === 'awaiting' ) awaiting = { id: it.id, activation: it.activation, schema: it.schema, target: it.target, dispatchedAt: it.dispatchedAt }
164
- else if( it.st === 'selected' ){
254
+ if( it.st === 'deliberating'){ blocking = true; deliberating = { id: it.id, schema: it.schema } }
255
+ else if( it.st === 'expanding') composite = { id: it.id, activation: it.activation, schema: it.schema }
256
+ else if( it.st === 'awaiting') awaiting = { id: it.id, activation: it.activation, schema: it.schema, target: it.target, dispatchedAt: it.dispatchedAt }
257
+ else if( it.st === 'selected'){
165
258
  const activeMacroSub = it.parentIntentId !== undefined && expandingParents.has( it.parentIntentId )
166
259
  if( !activeMacroSub ) blocking = true // standalone or orphan sub → finish it
167
260
  }
168
261
  }
169
262
 
170
- const busy = ( n: number ): EngineResult => ({ commands: { metrics: [
171
- [ 'agency.field.eligible', n ], [ 'agency.selection.busy', 1 ],
172
- ] } })
263
+ // ── Exafferent rupture (EXAFFERENCE P3) ──────────────────────
264
+ // How hard the world is pulling at us this tick (from P2's exafferent
265
+ // percepts), and the slow-moving `situation.stability` it erodes. Both are
266
+ // pure reads of frozen state; `stabMetrics` is empty on a never-ruptured run
267
+ // so the quiet path stays byte-identical to pre-P3.
268
+ const senseEvents = this._senseBuffer
269
+ this._senseBuffer = [] // consumed exactly once
270
+ const rupture = computeRupture( state, tick, senseEvents )
271
+ const prevStab = metric( state, 'situation.stability', 1 )
272
+ const nextStabRaw = clamp01( prevStab + STABILITY_RECOVERY * ( 1 - prevStab ) - rupture )
273
+ const nextStab = 1 - nextStabRaw < STABILITY_EPSILON ? 1 : nextStabRaw
274
+ const stabMetrics: Array<[ string, number ]> =
275
+ ( rupture > 0 || prevStab < 1 ) ? [ [ 'situation.stability', nextStab ] ] : []
276
+
277
+ if( rupture > 0 && this._bus ){
278
+ try {
279
+ this._bus.publish({
280
+ type: 'agency.situation.rupture', version: 1, sourceEngine: this.name,
281
+ salience: rupture, payload: { rupture, stability: nextStab, tick },
282
+ })
283
+ }
284
+ catch( err ){ logger.warn(`[selector] rupture publish failed: ${ err instanceof Error ? err.message : String( err ) }`) }
285
+ }
286
+
287
+ const busy = ( n: number ): EngineResult => ({
288
+ commands: {
289
+ metrics: [
290
+ [ 'agency.field.eligible', n ],
291
+ [ 'agency.selection.busy', 1 ],
292
+ ...stabMetrics,
293
+ ]
294
+ }
295
+ })
296
+
297
+ // ── Commitment revocation (EXAFFERENCE P4) ───────────────────
298
+ // A hard rupture doesn't just soften the switch cost — it lets go of a
299
+ // commitment still being weighed. We can't delete the `deliberating` intent
300
+ // here (Deliberation runs after us and would resurrect it set-after-delete),
301
+ // so we drop a tombstone the Deliberation engine + Executor honor next tick.
302
+ // No successor is committed — the field re-forms and the next tick selects.
303
+ if( deliberating && rupture >= RUPTURE_REVOKE_GATE ){
304
+ if( this._bus ){
305
+ try {
306
+ this._bus.publish({
307
+ type: 'agency.commitment.revoked', version: 1, sourceEngine: this.name,
308
+ salience: 0.85,
309
+ payload: { from: deliberating.schema, reason: 'exafferent-rupture', rupture, tick },
310
+ })
311
+ }
312
+ catch( err ){ logger.warn(`[selector] revoked publish failed: ${ err instanceof Error ? err.message : String( err ) }`) }
313
+ }
314
+ this._lastRevoked = { schema: deliberating.schema, tick } // Channel-B: flavor the next deliberation
315
+ return {
316
+ commands: {
317
+ set: [ revocationEntity( deliberating.id, deliberating.schema, rupture, tick ) ],
318
+ metrics: [
319
+ [ 'agency.field.eligible', eligible.length ],
320
+ [ 'agency.selection.busy', 1 ],
321
+ [ 'agency.commitment.revoked', 1 ],
322
+ ...stabMetrics,
323
+ ],
324
+ },
325
+ }
326
+ }
173
327
 
174
328
  if( blocking ) return busy( eligible.length )
175
329
 
176
330
  // Nothing afforded → idle (if an action is in flight, keep waiting).
177
331
  if( eligible.length === 0 )
178
- return { commands: { metrics: [ [ 'agency.field.eligible', 0 ], [ 'agency.selection.busy', ( awaiting || composite ) ? 1 : 0 ] ] } }
332
+ return {
333
+ commands: {
334
+ metrics: [
335
+ [ 'agency.field.eligible', 0 ],
336
+ [ 'agency.selection.busy',( awaiting || composite ) ? 1 : 0 ],
337
+ ...stabMetrics,
338
+ ]
339
+ }
340
+ }
179
341
 
180
342
  // ── Run the competition over the current field ────────────────
181
343
  const bias = buildBias( state )
@@ -183,7 +345,12 @@ export class ActionSelector implements CognitiveEngine {
183
345
  // base (engine-config-action-selector.switchCost ⊕ prior) hardened by how long the
184
346
  // Will has been committed (shared task_switch.current_focus_ticks). Computed once;
185
347
  // both preemption paths scale it DOWN by the challenger's stakes.
186
- const effSwitchCost = effectiveSwitchCost( state )
348
+ // Rupture softens engagement on top of the persona/focus base: an unstable
349
+ // situation makes the Will readier to be pulled off what it's doing. This is
350
+ // ORTHOGONAL to the per-challenger stakes scaling applied at each preemption
351
+ // site below (world-instability vs. challenger-quality) — the two compose,
352
+ // they don't double-count.
353
+ const effSwitchCost = effectiveSwitchCost( state ) * ( 1 - rupture )
187
354
  // Competition weights (base ⊕ prior): a steadier Will weighs risk less (bolder), an
188
355
  // open Will weighs novelty more (curiosity pulls toward the unpracticed). Other weights
189
356
  // stay at DEFAULT_WEIGHTS — only the two with a clean trait owner are developable.
@@ -195,27 +362,46 @@ export class ActionSelector implements CognitiveEngine {
195
362
  const winner = scored[0]
196
363
  if( !winner ) return busy( eligible.length )
197
364
 
198
- // ── Preempt a mid-composite routine (cancel-only) ─────────────
199
- // A strong/high-stakes challenger CANCELS the routine: delete the parent so the
200
- // executor's `_advance` guard (`if(!parent) return`) stops queueing sub-steps. We
201
- // do NOT commit the challenger this tick that would race the executor's in-tick
202
- // macro-advance and double-enact. The current sub drains, then the next free tick
203
- // selects. (Immediate-switch composite preemption needs deferred macro-advance.)
365
+ // ── Preempt a mid-composite routine (IMMEDIATE SWITCH) ────────
366
+ // A strong/high-stakes challenger cuts the routine off AND takes the body
367
+ // the same tick. We cannot delete the parent here: the executor runs later
368
+ // this tick off the pre-delete snapshot, and its macro-advance would
369
+ // re-`set` the parent (resurrecting it, set-after-delete) and queue the next
370
+ // sub as 'selected' two selected intents next tick = double enaction.
371
+ // So we tombstone the parent instead (the P4 mechanism) and commit the
372
+ // challenger: next tick the executor honours the tombstone, dropping the
373
+ // queued sub and the parent, and enacts the challenger alone. One body, one
374
+ // action, one tick saved over the old cancel-only path.
375
+ let compositeTombstone: string | undefined
376
+ let compositeFrom: string | undefined
204
377
  if( composite ){
205
378
  const different = winner.affordance.schema !== composite.schema
206
379
  const switchCost = effSwitchCost * ( 1 - stakes( winner.affordance, bias ) )
207
- if( different && winner.activation > composite.activation + switchCost ){
208
- if( this._bus ) try {
209
- this._bus.publish({ type: 'agency.action.preempted', version: 1, sourceEngine: this.name,
210
- salience: 0.8, payload: { from: composite.schema, to: winner.affordance.schema, activation: winner.activation, tick } })
211
- } catch( err ){ logger.warn( `[selector] preempt publish failed: ${ err instanceof Error ? err.message : String( err ) }` ) }
212
- return { commands: { delete: [ composite.id ], metrics: [
213
- [ 'agency.field.eligible', eligible.length ],
214
- [ 'agency.selection.busy', 1 ],
215
- [ 'agency.selection.preempted', 1 ],
216
- ] } }
217
- }
218
- return busy( eligible.length ) // routine continues
380
+ if( !different || winner.activation <= composite.activation + switchCost )
381
+ return busy( eligible.length ) // routine continues
382
+
383
+ if( this._bus )
384
+ try {
385
+ this._bus.publish({
386
+ type: 'agency.action.preempted',
387
+ version: 1,
388
+ sourceEngine: this.name,
389
+ salience: 0.8,
390
+ payload: {
391
+ from: composite.schema,
392
+ to: winner.affordance.schema,
393
+ activation: winner.activation,
394
+ tick
395
+ }
396
+ })
397
+ }
398
+ catch( err ){
399
+ logger.warn(`[selector] preempt publish failed: ${ err instanceof Error ? err.message : String( err ) }`)
400
+ }
401
+
402
+ compositeTombstone = composite.id
403
+ compositeFrom = composite.schema
404
+ // fall through to commit the challenger
219
405
  }
220
406
 
221
407
  // ── Preempt an awaiting action (commit the challenger) ────────
@@ -223,10 +409,12 @@ export class ActionSelector implements CognitiveEngine {
223
409
  // faces almost none, so salient events interrupt. Deleting an 'awaiting' intent is
224
410
  // race-free: the executor never enacts it (only times it out at 15 ticks).
225
411
  let preemptDelete: string | undefined
226
- let preemptedFrom: string | undefined
227
- if( awaiting ){
228
- const sameAction = winner.affordance.schema === awaiting.schema
229
- && ( winner.affordance.targetEntityId ?? '' ) === awaiting.target
412
+ let preemptedFrom: string | undefined = compositeFrom
413
+ // A composite preemption already claimed the body: its tombstone cancels the
414
+ // whole macro (parent + any sub, whatever the sub's status), so the awaiting
415
+ // hysteresis below which is about a *standalone* awaiting action — is skipped.
416
+ if( awaiting && !compositeTombstone ){
417
+ const sameAction = winner.affordance.schema === awaiting.schema && ( winner.affordance.targetEntityId ?? '') === awaiting.target
230
418
  if( sameAction ) return busy( eligible.length ) // field still favours what we await
231
419
 
232
420
  const staleness = Math.min( 1, ( tick - awaiting.dispatchedAt ) / AWAIT_STALE_TICKS )
@@ -253,7 +441,7 @@ export class ActionSelector implements CognitiveEngine {
253
441
  // deliberativeness disposition (R1) shifts both gates the OTHER way for an analytical
254
442
  // Will — wider margin gate (contest sooner) and lower stakes gate (deliberate at lower
255
443
  // stakes). The two forces compose additively, each independent and bounded.
256
- const deliberativeness = -( readPersonaPrior( state, 'engine-config-executive' )['deliberateThreshold'] ?? 0 )
444
+ const deliberativeness = -( readPersonaPrior( state, 'engine-config-executive')['deliberateThreshold'] ?? 0 )
257
445
  const marginGate = Math.max( 0, MARGIN_THRESHOLD - relief + deliberativeness * DELIB_MARGIN_SENS )
258
446
  const stakesGate = clamp01( BASE_STAKES_THRESHOLD + relief - deliberativeness * DELIB_STAKES_SENS )
259
447
  const deliberate = margin < marginGate || stk > stakesGate
@@ -261,6 +449,17 @@ export class ActionSelector implements CognitiveEngine {
261
449
  this._lastEntropy = entropy
262
450
  this._lastDeliberate = deliberate
263
451
 
452
+ // Channel-B revocation hint: if we let go of a commitment under rupture in the
453
+ // last few ticks and are now forming a fresh deliberation, tell it so — then
454
+ // forget (consumed once). Stale hints are cleared without stamping.
455
+ let revokedBy: string | undefined
456
+ if( this._lastRevoked ){
457
+ if( deliberate && tick - this._lastRevoked.tick <= REVOKE_HINT_WINDOW )
458
+ revokedBy = this._lastRevoked.schema
459
+ if( revokedBy || tick - this._lastRevoked.tick > REVOKE_HINT_WINDOW )
460
+ this._lastRevoked = null
461
+ }
462
+
264
463
  const intent: EntityInput = {
265
464
  id: `agency-intent-${ tick }`,
266
465
  type: 'agency.intent',
@@ -305,6 +504,9 @@ export class ActionSelector implements CognitiveEngine {
305
504
  // interrupted, so the deliberation facet can own the interruption in-character
306
505
  // ("I was about to X, but this pulled me away"). Only meaningful while deliberating.
307
506
  ...( preemptedFrom ? { preemptedFrom } : {} ),
507
+ // Channel B: this deliberation forms in the wake of a rupture-driven
508
+ // revocation — the facet can own "something changed, I let go of X".
509
+ ...( revokedBy ? { revokedBy } : {} ),
308
510
  tick,
309
511
  },
310
512
  }
@@ -318,6 +520,7 @@ export class ActionSelector implements CognitiveEngine {
318
520
  salience: 0.6,
319
521
  payload: { schema: winner.affordance.schema, activation: winner.activation, entropy, tick },
320
522
  })
523
+
321
524
  if( deliberate )
322
525
  this._bus.publish({
323
526
  type: 'agency.selection.ambiguous',
@@ -329,6 +532,7 @@ export class ActionSelector implements CognitiveEngine {
329
532
  entropy, stakes: stk, tick,
330
533
  },
331
534
  })
535
+
332
536
  if( preemptedFrom )
333
537
  this._bus.publish({
334
538
  type: 'agency.action.preempted',
@@ -339,12 +543,16 @@ export class ActionSelector implements CognitiveEngine {
339
543
  })
340
544
  }
341
545
  catch( err ){
342
- logger.warn( `[selector] bus publish failed: ${ err instanceof Error ? err.message : String( err ) }` )
546
+ logger.warn(`[selector] bus publish failed: ${ err instanceof Error ? err.message : String( err ) }`)
343
547
  }
344
548
  }
345
549
 
346
550
  const commands: StateCommands = {
347
- set: [ intent ],
551
+ // A composite preemption rides along as a tombstone (never a delete — the
552
+ // executor's in-tick macro-advance would resurrect the parent).
553
+ set: compositeTombstone
554
+ ? [ intent, revocationEntity( compositeTombstone, compositeFrom ?? '', rupture, tick ) ]
555
+ : [ intent ],
348
556
  ...( preemptDelete ? { delete: [ preemptDelete ] } : {} ),
349
557
  metrics: [
350
558
  [ 'agency.field.eligible', eligible.length ],
@@ -354,8 +562,10 @@ export class ActionSelector implements CognitiveEngine {
354
562
  [ 'agency.selection.deliberate', deliberate ? 1 : 0 ],
355
563
  [ 'agency.selection.preempted', preemptedFrom ? 1 : 0 ],
356
564
  [ 'agency.selection.activation', winner.activation ],
565
+ ...stabMetrics,
357
566
  ],
358
567
  }
568
+
359
569
  return { commands }
360
570
  }
361
571
  }
@@ -373,10 +583,53 @@ export class ActionSelector implements CognitiveEngine {
373
583
  * TaskSwitcher uses internally. One disposition, two owners, native scales.
374
584
  */
375
585
  function effectiveSwitchCost( state: ReadonlySimulationState ): number {
376
- const params = readEffectiveParams( state, 'engine-config-action-selector' )
586
+ const params = readEffectiveParams( state, 'engine-config-action-selector')
377
587
  const base = num( params['switchCost'], BASE_SWITCH_COST )
378
588
  const focusTicks = metric( state, 'task_switch.current_focus_ticks', 0 )
379
- return base * ( 1 + focusTicks * FOCUS_GAIN )
589
+ // EXAFFERENCE P3 focus only hardens under a stable situation. `situation.stability`
590
+ // (absent ⇒ 1 ⇒ pre-P3 behavior) scales the focus-hardening term, so a Will in a
591
+ // destabilized world can't cling to a long-held focus. The fast, same-tick softener
592
+ // is `(1 - rupture)` applied at the call site; this is the slow, persistent one.
593
+ const stability = metric( state, 'situation.stability', 1 )
594
+ return base * ( 1 + focusTicks * FOCUS_GAIN * stability )
595
+ }
596
+
597
+ /**
598
+ * EXAFFERENCE P3 — the exafferent-rupture scalar. Max salience among the
599
+ * `provenance:'exafferent'` percepts fresh within `RUPTURE_WINDOW_TICKS`,
600
+ * mapped through `RUPTURE_SALIENCE_GATE` into [0,1]. Pure read of frozen state;
601
+ * `reafferent` percepts (our own echo, P2) are excluded by construction, so the
602
+ * mind is never ruptured by itself.
603
+ */
604
+ function computeRupture(
605
+ state: ReadonlySimulationState,
606
+ tick: Tick,
607
+ senseEvents: ReadonlyArray<{ salience: number; text?: string }> = [],
608
+ ): number {
609
+ let maxSalience = 0
610
+ for( const e of state.entities.values() ){
611
+ if( e.type !== 'percept') continue
612
+ const m = e.metadata
613
+ if( str( m?.['provenance'] ) !== 'exafferent') continue
614
+ const pTick = num( m?.['tick'], -1 )
615
+ if( pTick < 0 || tick - pTick > RUPTURE_WINDOW_TICKS ) continue
616
+ const s = num( m?.['salience'], 0 )
617
+ if( s > maxSalience ) maxSalience = s
618
+ }
619
+
620
+ // ACP §2b — buffered sense-channel percepts (bus-only; never entities). The
621
+ // echo guard extends to this path: an event whose text matches a live
622
+ // consequence descriptor is our own action coming back and cannot rupture.
623
+ if( senseEvents.length > 0 ){
624
+ const live = liveConsequences( state.entities, tick )
625
+ for( const ev of senseEvents ){
626
+ if( ev.text !== undefined && live.length > 0 && matchConsequenceText( live, ev.text ) ) continue
627
+ if( ev.salience > maxSalience ) maxSalience = ev.salience
628
+ }
629
+ }
630
+
631
+ if( maxSalience <= RUPTURE_SALIENCE_GATE ) return 0
632
+ return clamp01( ( maxSalience - RUPTURE_SALIENCE_GATE ) / ( 1 - RUPTURE_SALIENCE_GATE ) )
380
633
  }
381
634
 
382
635
  /**
@@ -386,7 +639,7 @@ function effectiveSwitchCost( state: ReadonlySimulationState ): number {
386
639
  * weights have no clean single-trait owner, so they stay fixed (see the TODO catalogue).
387
640
  */
388
641
  function effectiveWeights( state: ReadonlySimulationState ): ScoreWeights {
389
- const p = readEffectiveParams( state, 'engine-config-action-selector' )
642
+ const p = readEffectiveParams( state, 'engine-config-action-selector')
390
643
  return {
391
644
  ...DEFAULT_WEIGHTS,
392
645
  risk: Math.max( 0, num( p['riskWeight'], DEFAULT_WEIGHTS.risk ) ),
@@ -404,10 +657,10 @@ function buildBias( state: ReadonlySimulationState ): BiasContext {
404
657
  let maxGoalPriority = 0
405
658
 
406
659
  for( const e of state.entities.values() ){
407
- if( e.type !== 'goal' ) continue
660
+ if( e.type !== 'goal') continue
408
661
  const m = e.metadata
409
662
  const status = str( m?.['status'] )
410
- if( status !== 'active' && status !== 'in_progress' ) continue
663
+ if( status !== 'active' && status !== 'in_progress') continue
411
664
  maxGoalPriority = Math.max( maxGoalPriority, num( m?.['priority'], 0 ) )
412
665
  }
413
666
 
@@ -441,7 +694,7 @@ function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<st
441
694
  cost: num( meta['cost'], 0 ),
442
695
  habitStrength: num( meta['habitStrength'], 0 ),
443
696
  available: meta['available'] === true,
444
- tags: Array.isArray( meta['tags'] ) ? ( meta['tags'] as unknown[] ).filter( ( t ): t is string => typeof t === 'string' ) : [],
697
+ tags: Array.isArray( meta['tags'] ) ? ( meta['tags'] as unknown[] ).filter( ( t ): t is string => typeof t === 'string') : [],
445
698
  planBias: typeof meta['planBias'] === 'number' ? ( meta['planBias'] as number ) : undefined,
446
699
  planId: str( meta['planId'] ),
447
700
  stepId: str( meta['stepId'] ),
@@ -114,14 +114,12 @@ export class AffordanceSynthesizer implements CognitiveEngine {
114
114
 
115
115
  // Clear the previous tick's field — affordances are transient.
116
116
  for( const [ id, e ] of state.entities )
117
- if( e.type === 'affordance' ) del.push( id )
117
+ if( e.type === 'affordance') del.push( id )
118
118
 
119
119
  // ── 1. innate floor — always emitted, never attention-capped ──
120
- const floor = schemas.filter( s => s.binds === 'none' )
120
+ const floor = schemas.filter( s => s.binds === 'none')
121
121
  for( const schema of floor )
122
- set.push( this._toEntity(
123
- this._build( schema, tick, state, valence, energyLow, skills, {} ),
124
- ) )
122
+ set.push( this._toEntity( this._build( schema, tick, state, valence, energyLow, skills, {} ) ) )
125
123
 
126
124
  // ── 2. perception-bound candidates, attention-budgeted ───────
127
125
  // Goal-relevance counts at the CAP, not only at selection: an affordance whose
@@ -131,14 +129,16 @@ export class AffordanceSynthesizer implements CognitiveEngine {
131
129
  const candidates: Candidate[] = []
132
130
  const goalTargets: Map<string, number> = collectGoalTargets( state )
133
131
 
134
- const perceptSchema = schemas.find( s => s.binds === 'percept' )
132
+ const perceptSchema = schemas.find( s => s.binds === 'percept')
135
133
  if( perceptSchema )
136
134
  for( const [ id, e ] of state.entities ){
137
- if( e.type !== 'percept' ) continue
135
+ if( e.type !== 'percept') continue
136
+
138
137
  const m = e.metadata
139
138
  const salience = num( m?.['salience'], 0 )
140
139
  const summary = str( m?.['summary'] ) ?? str( m?.['category'] ) ?? 'something'
141
140
  const target = str( m?.['entityId'] ) ?? str( m?.['targetEntityId'] )
141
+
142
142
  candidates.push({
143
143
  salience: salience + ( target ? goalTargets.get( target ) ?? 0 : 0 ),
144
144
  affordance: this._build( perceptSchema, tick, state, valence, energyLow, skills, {
@@ -155,21 +155,26 @@ export class AffordanceSynthesizer implements CognitiveEngine {
155
155
  // things — so the Will can direct `give`/`greet` at someone or `use`/`pick-up`
156
156
  // at something in particular. Each (schema × target) enters as a candidate at
157
157
  // the target's salience and competes through the attention cap.
158
- const personSchemas = schemas.filter( s => s.binds === 'entity' )
159
- const objectSchemas = schemas.filter( s => s.binds === 'object' )
158
+ const personSchemas = schemas.filter( s => s.binds === 'entity')
159
+ const objectSchemas = schemas.filter( s => s.binds === 'object')
160
+
160
161
  if( personSchemas.length > 0 || objectSchemas.length > 0 )
161
162
  for( const [ id, e ] of state.entities ){
162
- if( e.type !== 'known-entity' ) continue
163
+ if( e.type !== 'known-entity') continue
164
+
163
165
  const m = e.metadata
164
166
  const kind = str( m?.['kind'] )
165
167
  const applicable = kind === 'sentient' ? personSchemas : kind === 'thing' ? objectSchemas : null
168
+
166
169
  if( !applicable || applicable.length === 0 ) continue
170
+
167
171
  const keid = str( m?.['keid'] ) ?? id
168
172
  const fam = num( m?.['familiarity'], 0 )
169
173
  const val = num( m?.['valence'], 0 )
170
174
  const res = num( m?.['resolutionConfidence'], 0 )
171
175
  const salience = fam * 0.6 + Math.max( 0, val ) * 0.3 + res * 0.1 + ( goalTargets.get( keid ) ?? 0 )
172
176
  const name = str( m?.['name'] ) ?? keid
177
+
173
178
  for( const schema of applicable )
174
179
  candidates.push({
175
180
  salience,
@@ -188,11 +193,14 @@ export class AffordanceSynthesizer implements CognitiveEngine {
188
193
  // it. This is the AffordanceSource.ideomotor leg: executive intention → an
189
194
  // affordance that competes like any other.
190
195
  for( const [ id, e ] of state.entities ){
191
- if( e.type !== 'ideomotor.intent' ) continue
196
+ if( e.type !== 'ideomotor.intent') continue
197
+
192
198
  const m = e.metadata
193
199
  const schemaId = str( m?.['schema'] )
194
200
  const schema = schemaId ? schemas.find( s => s.id === schemaId ) : undefined
201
+
195
202
  if( !schema ) continue
203
+
196
204
  candidates.push({
197
205
  salience: IDEOMOTOR_BASE_SALIENCE + num( m?.['priority'], 0.8 ),
198
206
  affordance: this._build( schema, tick, state, valence, energyLow, skills, {
@@ -215,11 +223,14 @@ export class AffordanceSynthesizer implements CognitiveEngine {
215
223
  // next tick. The suggested schema must resolve in the repertoire; if it does not,
216
224
  // the prior cannot surface as an action and the plan waits / replans.
217
225
  for( const [ id, e ] of state.entities ){
218
- if( e.type !== 'plan.prior' ) continue
226
+ if( e.type !== 'plan.prior') continue
227
+
219
228
  const m = e.metadata
220
229
  const schemaId = str( m?.['schema'] )
221
230
  const schema = schemaId ? schemas.find( s => s.id === schemaId ) : undefined
231
+
222
232
  if( !schema ) continue
233
+
223
234
  const planBias = clamp01( num( m?.['planBias'], 0.6 ) )
224
235
  candidates.push({
225
236
  salience: IDEOMOTOR_BASE_SALIENCE + planBias,
@@ -266,7 +277,7 @@ export class AffordanceSynthesizer implements CognitiveEngine {
266
277
  })
267
278
  }
268
279
  catch( err ){
269
- logger.warn( `[affordance] bus publish failed: ${ err instanceof Error ? err.message : String( err ) }` )
280
+ logger.warn(`[affordance] bus publish failed: ${ err instanceof Error ? err.message : String( err ) }`)
270
281
  }
271
282
  }
272
283
 
@@ -296,8 +307,7 @@ export class AffordanceSynthesizer implements CognitiveEngine {
296
307
  const skill = skills?.get( schema.id )
297
308
 
298
309
  // Learned value if known, else the schema's intrinsic prior mapped to 0..1.
299
- const expectedReward = skill?.valueEstimate
300
- ?? clamp01( ( ( schema.baseValence ?? 0 ) + 1 ) / 2 )
310
+ const expectedReward = skill?.valueEstimate ?? clamp01( ( ( schema.baseValence ?? 0 ) + 1 ) / 2 )
301
311
  const expectedValence = schema.baseValence ?? valence
302
312
  const habitStrength = skill?.habitStrength ?? 0
303
313
  // Low energy makes everything feel costlier.
@@ -374,7 +384,7 @@ export class AffordanceSynthesizer implements CognitiveEngine {
374
384
  }
375
385
 
376
386
  private _attentionCap( state: ReadonlySimulationState ): number {
377
- const cap = state.metrics.get( 'attention.capacity' ) ?? this._defaultCap
387
+ const cap = state.metrics.get('attention.capacity') ?? this._defaultCap
378
388
  return Math.max( 1, Math.min( MAX_ATTENTION_CAP, Math.round( cap ) ) )
379
389
  }
380
390
  }