@mindot/will 0.5.0 → 0.7.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 (136) hide show
  1. package/dist/channels/discord.d.ts +1 -1
  2. package/dist/channels/discord.js.map +1 -1
  3. package/dist/channels/whatsapp.d.ts +1 -1
  4. package/dist/channels/whatsapp.js.map +1 -1
  5. package/dist/cli.js +998 -59
  6. package/dist/cli.js.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +998 -59
  9. package/dist/index.js.map +1 -1
  10. package/dist/mcp/effectors.d.ts +1 -1
  11. package/dist/mcp/effectors.js.map +1 -1
  12. package/dist/{will-BDq-TMQr.d.ts → will-DAW0l-lY.d.ts} +90 -10
  13. package/package.json +1 -1
  14. package/src/channels/discord.ts +11 -11
  15. package/src/channels/roster.ts +1 -1
  16. package/src/channels/types.ts +1 -1
  17. package/src/channels/whatsapp.ts +25 -25
  18. package/src/cli.ts +20 -20
  19. package/src/cognition/acp.ts +28 -0
  20. package/src/cognition/agency/consequence.ts +174 -0
  21. package/src/cognition/agency/engines/action.selector.ts +333 -48
  22. package/src/cognition/agency/engines/affordance.synthesizer.ts +35 -16
  23. package/src/cognition/agency/engines/deliberation.engine.ts +28 -14
  24. package/src/cognition/agency/engines/motor.schema.executor.ts +90 -14
  25. package/src/cognition/agency/engines/reafference.engine.ts +152 -23
  26. package/src/cognition/agency/execution.primitives.ts +13 -13
  27. package/src/cognition/agency/proactive.communicator.ts +1 -1
  28. package/src/cognition/agency/reconcile.learning.ts +10 -1
  29. package/src/cognition/agency/revocation.ts +79 -0
  30. package/src/cognition/agency/schemas/repertoire.ts +109 -9
  31. package/src/cognition/agency/selection.scoring.ts +32 -10
  32. package/src/cognition/agency/types.ts +9 -0
  33. package/src/cognition/bus.ts +2 -2
  34. package/src/cognition/completion.inbox.ts +1 -1
  35. package/src/cognition/config.mirror.entities.ts +5 -5
  36. package/src/cognition/event.log.ts +2 -2
  37. package/src/cognition/event.schemas.ts +67 -67
  38. package/src/cognition/faculties/aesthetic.evaluator.ts +6 -6
  39. package/src/cognition/faculties/affective.blender.ts +36 -4
  40. package/src/cognition/faculties/attachment.evaluator.ts +6 -6
  41. package/src/cognition/faculties/attention.allocator.ts +63 -8
  42. package/src/cognition/faculties/autobiographical.narrator.ts +3 -3
  43. package/src/cognition/faculties/bias.detector.ts +7 -7
  44. package/src/cognition/faculties/circadian.oscillator.ts +3 -3
  45. package/src/cognition/faculties/confidence.calibrator.ts +7 -7
  46. package/src/cognition/faculties/dream.simulator.ts +5 -5
  47. package/src/cognition/faculties/empathy.simulator.ts +7 -7
  48. package/src/cognition/faculties/energy.regulator.ts +4 -4
  49. package/src/cognition/faculties/episodic.consolidator.ts +6 -6
  50. package/src/cognition/faculties/executive.engine/commands.ts +6 -6
  51. package/src/cognition/faculties/executive.engine/context.ts +42 -42
  52. package/src/cognition/faculties/executive.engine/deferred.effects.ts +2 -2
  53. package/src/cognition/faculties/executive.engine/engine.ts +25 -25
  54. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +12 -12
  55. package/src/cognition/faculties/executive.engine/facet.ts +18 -18
  56. package/src/cognition/faculties/executive.engine/gating.ts +2 -2
  57. package/src/cognition/faculties/executive.engine/messages.ts +3 -3
  58. package/src/cognition/faculties/executive.engine/parser.ts +11 -11
  59. package/src/cognition/faculties/executive.engine/prompt.factory.ts +55 -55
  60. package/src/cognition/faculties/exteroception.ts +99 -5
  61. package/src/cognition/faculties/forgetting.curve.ts +3 -3
  62. package/src/cognition/faculties/frustration.evaluator.ts +6 -6
  63. package/src/cognition/faculties/goal.manager.ts +15 -15
  64. package/src/cognition/faculties/inhibition.controller.ts +3 -3
  65. package/src/cognition/faculties/interoception.ts +3 -3
  66. package/src/cognition/faculties/introspection.engine.ts +3 -3
  67. package/src/cognition/faculties/known.entity.tracker.ts +15 -15
  68. package/src/cognition/faculties/loss.evaluator.ts +2 -2
  69. package/src/cognition/faculties/moral.evaluator.ts +4 -4
  70. package/src/cognition/faculties/novelty.detector.ts +4 -4
  71. package/src/cognition/faculties/persona.consolidator.ts +12 -12
  72. package/src/cognition/faculties/planning.engine/engine.ts +47 -47
  73. package/src/cognition/faculties/planning.engine/plan.frontier.ts +5 -5
  74. package/src/cognition/faculties/planning.engine/plan.store.ts +1 -1
  75. package/src/cognition/faculties/planning.engine/plan.supervision.ts +20 -20
  76. package/src/cognition/faculties/reputation.tracker.ts +6 -6
  77. package/src/cognition/faculties/reward.evaluator.ts +4 -4
  78. package/src/cognition/faculties/self.model.updater.ts +8 -8
  79. package/src/cognition/faculties/semantic.engine/integrator.ts +15 -15
  80. package/src/cognition/faculties/sleep.pressure.regulator.ts +4 -4
  81. package/src/cognition/faculties/social.perception.ts +3 -3
  82. package/src/cognition/faculties/spaced.repetition.ts +2 -2
  83. package/src/cognition/faculties/stress.regulator.ts +27 -4
  84. package/src/cognition/faculties/task.switcher.ts +11 -6
  85. package/src/cognition/faculties/theory.of.mind.ts +6 -6
  86. package/src/cognition/faculties/threat.evaluator.ts +3 -3
  87. package/src/cognition/faculties/working.memory.ts +8 -8
  88. package/src/cognition/generative.model.ts +2 -2
  89. package/src/cognition/memory/vector.adapter.ts +5 -5
  90. package/src/cognition/memory/vector.content.ts +4 -4
  91. package/src/cognition/memory/vector.embedder.ts +9 -9
  92. package/src/cognition/persona.prior.ts +1 -1
  93. package/src/cognition/schema.registry.ts +2 -2
  94. package/src/cognition/senses/audition.engine/engine.ts +2 -2
  95. package/src/cognition/senses/base.sense.engine.ts +1 -1
  96. package/src/cognition/utilities/token.tracker.ts +5 -5
  97. package/src/core/abstracts.ts +1 -1
  98. package/src/core/completion.recorder.ts +2 -2
  99. package/src/core/serialization.ts +3 -3
  100. package/src/core/snapshot.manager.ts +1 -1
  101. package/src/core/state.manager.ts +1 -1
  102. package/src/core/utils.ts +1 -1
  103. package/src/host/boot.ts +30 -30
  104. package/src/host/utterances.ts +1 -1
  105. package/src/llm/index.ts +9 -9
  106. package/src/llm/summarizer.ts +4 -4
  107. package/src/llm/wire.contracts.ts +1 -1
  108. package/src/mcp/effectors.ts +10 -10
  109. package/src/mcp/server.ts +11 -11
  110. package/src/pma/eval.ts +1 -1
  111. package/src/pma/index.ts +21 -21
  112. package/src/runners/outreach.runner.ts +3 -3
  113. package/src/runners/social.runner.ts +1 -1
  114. package/src/runners/thin-shim.runner.ts +1 -1
  115. package/src/sdk/will.ts +5 -5
  116. package/src/serve/server.ts +12 -12
  117. package/src/stem/assembly.audit.ts +3 -3
  118. package/src/stem/guards/identity.coherence.ts +10 -10
  119. package/src/stem/guards/identity.guard.ts +22 -22
  120. package/src/stem/index.ts +26 -11
  121. package/src/stem/mind.ts +12 -12
  122. package/src/stem/policy/arbiter.ts +136 -0
  123. package/src/stem/policy/rule.table.ts +172 -0
  124. package/src/stem/policy/verdict.recorder.ts +0 -0
  125. package/src/stem/tracts/biography.writer.ts +3 -3
  126. package/src/stem/tracts/effector.controller.ts +383 -4
  127. package/src/stem/tracts/health.reporter.ts +1 -1
  128. package/src/stem/tracts/outbox.controller.ts +4 -0
  129. package/src/stem/tracts/outbox.writer.ts +4 -4
  130. package/src/stem/tracts/pma.controller.ts +1 -1
  131. package/src/stem/tracts/replay.controller.ts +10 -10
  132. package/src/stem/tracts/sensory.controller.ts +2 -2
  133. package/src/stem/tracts/session.logger.ts +4 -4
  134. package/src/stem/tracts/transport/socketio.transport.ts +7 -7
  135. package/src/stem/tracts/transport/stream.transport.ts +2 -2
  136. 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 },
111
148
  ]
112
149
  }
113
- subscribes(): string[] { return [] }
114
- onCognitiveEvent(): void { /* pull model — reads the field from frozen state */ }
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',
159
+ ]
160
+ }
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,104 @@ 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 · POLICY_REAFFERENCE P3) ──
298
+ // A commitment still being weighed is let go for either of two DISTINCT
299
+ // reasons: a hard exafferent rupture (the world surprised us), or a class
300
+ // policy refusal of the very schema we're deliberating (the boundary just
301
+ // declared it forbidden — deliberating our way into it is wasted). The two
302
+ // never mix: the refusal is an outcome, not a percept, so it contributes
303
+ // ZERO to the exafferent scalar. Either way we can't delete the
304
+ // `deliberating` intent here (Deliberation runs after us and would resurrect
305
+ // it set-after-delete), so we drop a tombstone the Deliberation engine +
306
+ // Executor honor next tick. No successor is committed — the field re-forms.
307
+ const policyRevoke = !!deliberating && refusedClassSchemas( state ).has( deliberating.schema )
308
+ if( deliberating && ( rupture >= RUPTURE_REVOKE_GATE || policyRevoke ) ){
309
+ const reason = policyRevoke ? 'policy-refusal' : 'exafferent-rupture'
310
+ const revRupture = policyRevoke ? Math.max( rupture, RUPTURE_REVOKE_GATE ) : rupture
311
+ if( this._bus ){
312
+ try {
313
+ this._bus.publish({
314
+ type: 'agency.commitment.revoked', version: 1, sourceEngine: this.name,
315
+ salience: 0.85,
316
+ payload: { from: deliberating.schema, reason, rupture: revRupture, tick },
317
+ })
318
+ }
319
+ catch( err ){ logger.warn(`[selector] revoked publish failed: ${ err instanceof Error ? err.message : String( err ) }`) }
320
+ }
321
+ this._lastRevoked = { schema: deliberating.schema, tick } // Channel-B: flavor the next deliberation
322
+ return {
323
+ commands: {
324
+ set: [ revocationEntity( deliberating.id, deliberating.schema, revRupture, tick ) ],
325
+ metrics: [
326
+ [ 'agency.field.eligible', eligible.length ],
327
+ [ 'agency.selection.busy', 1 ],
328
+ [ 'agency.commitment.revoked', 1 ],
329
+ ...( policyRevoke ? [ [ 'agency.policy.revoked', 1 ] as [ string, number ] ] : [] ),
330
+ ...stabMetrics,
331
+ ],
332
+ },
333
+ }
334
+ }
173
335
 
174
336
  if( blocking ) return busy( eligible.length )
175
337
 
176
338
  // Nothing afforded → idle (if an action is in flight, keep waiting).
177
339
  if( eligible.length === 0 )
178
- return { commands: { metrics: [ [ 'agency.field.eligible', 0 ], [ 'agency.selection.busy', ( awaiting || composite ) ? 1 : 0 ] ] } }
340
+ return {
341
+ commands: {
342
+ metrics: [
343
+ [ 'agency.field.eligible', 0 ],
344
+ [ 'agency.selection.busy',( awaiting || composite ) ? 1 : 0 ],
345
+ ...stabMetrics,
346
+ ]
347
+ }
348
+ }
179
349
 
180
350
  // ── Run the competition over the current field ────────────────
181
351
  const bias = buildBias( state )
@@ -183,7 +353,12 @@ export class ActionSelector implements CognitiveEngine {
183
353
  // base (engine-config-action-selector.switchCost ⊕ prior) hardened by how long the
184
354
  // Will has been committed (shared task_switch.current_focus_ticks). Computed once;
185
355
  // both preemption paths scale it DOWN by the challenger's stakes.
186
- const effSwitchCost = effectiveSwitchCost( state )
356
+ // Rupture softens engagement on top of the persona/focus base: an unstable
357
+ // situation makes the Will readier to be pulled off what it's doing. This is
358
+ // ORTHOGONAL to the per-challenger stakes scaling applied at each preemption
359
+ // site below (world-instability vs. challenger-quality) — the two compose,
360
+ // they don't double-count.
361
+ const effSwitchCost = effectiveSwitchCost( state ) * ( 1 - rupture )
187
362
  // Competition weights (base ⊕ prior): a steadier Will weighs risk less (bolder), an
188
363
  // open Will weighs novelty more (curiosity pulls toward the unpracticed). Other weights
189
364
  // stay at DEFAULT_WEIGHTS — only the two with a clean trait owner are developable.
@@ -195,27 +370,46 @@ export class ActionSelector implements CognitiveEngine {
195
370
  const winner = scored[0]
196
371
  if( !winner ) return busy( eligible.length )
197
372
 
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.)
373
+ // ── Preempt a mid-composite routine (IMMEDIATE SWITCH) ────────
374
+ // A strong/high-stakes challenger cuts the routine off AND takes the body
375
+ // the same tick. We cannot delete the parent here: the executor runs later
376
+ // this tick off the pre-delete snapshot, and its macro-advance would
377
+ // re-`set` the parent (resurrecting it, set-after-delete) and queue the next
378
+ // sub as 'selected' two selected intents next tick = double enaction.
379
+ // So we tombstone the parent instead (the P4 mechanism) and commit the
380
+ // challenger: next tick the executor honours the tombstone, dropping the
381
+ // queued sub and the parent, and enacts the challenger alone. One body, one
382
+ // action, one tick saved over the old cancel-only path.
383
+ let compositeTombstone: string | undefined
384
+ let compositeFrom: string | undefined
204
385
  if( composite ){
205
386
  const different = winner.affordance.schema !== composite.schema
206
387
  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
388
+ if( !different || winner.activation <= composite.activation + switchCost )
389
+ return busy( eligible.length ) // routine continues
390
+
391
+ if( this._bus )
392
+ try {
393
+ this._bus.publish({
394
+ type: 'agency.action.preempted',
395
+ version: 1,
396
+ sourceEngine: this.name,
397
+ salience: 0.8,
398
+ payload: {
399
+ from: composite.schema,
400
+ to: winner.affordance.schema,
401
+ activation: winner.activation,
402
+ tick
403
+ }
404
+ })
405
+ }
406
+ catch( err ){
407
+ logger.warn(`[selector] preempt publish failed: ${ err instanceof Error ? err.message : String( err ) }`)
408
+ }
409
+
410
+ compositeTombstone = composite.id
411
+ compositeFrom = composite.schema
412
+ // fall through to commit the challenger
219
413
  }
220
414
 
221
415
  // ── Preempt an awaiting action (commit the challenger) ────────
@@ -223,10 +417,12 @@ export class ActionSelector implements CognitiveEngine {
223
417
  // faces almost none, so salient events interrupt. Deleting an 'awaiting' intent is
224
418
  // race-free: the executor never enacts it (only times it out at 15 ticks).
225
419
  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
420
+ let preemptedFrom: string | undefined = compositeFrom
421
+ // A composite preemption already claimed the body: its tombstone cancels the
422
+ // whole macro (parent + any sub, whatever the sub's status), so the awaiting
423
+ // hysteresis below which is about a *standalone* awaiting action — is skipped.
424
+ if( awaiting && !compositeTombstone ){
425
+ const sameAction = winner.affordance.schema === awaiting.schema && ( winner.affordance.targetEntityId ?? '') === awaiting.target
230
426
  if( sameAction ) return busy( eligible.length ) // field still favours what we await
231
427
 
232
428
  const staleness = Math.min( 1, ( tick - awaiting.dispatchedAt ) / AWAIT_STALE_TICKS )
@@ -253,7 +449,7 @@ export class ActionSelector implements CognitiveEngine {
253
449
  // deliberativeness disposition (R1) shifts both gates the OTHER way for an analytical
254
450
  // Will — wider margin gate (contest sooner) and lower stakes gate (deliberate at lower
255
451
  // stakes). The two forces compose additively, each independent and bounded.
256
- const deliberativeness = -( readPersonaPrior( state, 'engine-config-executive' )['deliberateThreshold'] ?? 0 )
452
+ const deliberativeness = -( readPersonaPrior( state, 'engine-config-executive')['deliberateThreshold'] ?? 0 )
257
453
  const marginGate = Math.max( 0, MARGIN_THRESHOLD - relief + deliberativeness * DELIB_MARGIN_SENS )
258
454
  const stakesGate = clamp01( BASE_STAKES_THRESHOLD + relief - deliberativeness * DELIB_STAKES_SENS )
259
455
  const deliberate = margin < marginGate || stk > stakesGate
@@ -261,6 +457,17 @@ export class ActionSelector implements CognitiveEngine {
261
457
  this._lastEntropy = entropy
262
458
  this._lastDeliberate = deliberate
263
459
 
460
+ // Channel-B revocation hint: if we let go of a commitment under rupture in the
461
+ // last few ticks and are now forming a fresh deliberation, tell it so — then
462
+ // forget (consumed once). Stale hints are cleared without stamping.
463
+ let revokedBy: string | undefined
464
+ if( this._lastRevoked ){
465
+ if( deliberate && tick - this._lastRevoked.tick <= REVOKE_HINT_WINDOW )
466
+ revokedBy = this._lastRevoked.schema
467
+ if( revokedBy || tick - this._lastRevoked.tick > REVOKE_HINT_WINDOW )
468
+ this._lastRevoked = null
469
+ }
470
+
264
471
  const intent: EntityInput = {
265
472
  id: `agency-intent-${ tick }`,
266
473
  type: 'agency.intent',
@@ -305,6 +512,9 @@ export class ActionSelector implements CognitiveEngine {
305
512
  // interrupted, so the deliberation facet can own the interruption in-character
306
513
  // ("I was about to X, but this pulled me away"). Only meaningful while deliberating.
307
514
  ...( preemptedFrom ? { preemptedFrom } : {} ),
515
+ // Channel B: this deliberation forms in the wake of a rupture-driven
516
+ // revocation — the facet can own "something changed, I let go of X".
517
+ ...( revokedBy ? { revokedBy } : {} ),
308
518
  tick,
309
519
  },
310
520
  }
@@ -318,6 +528,7 @@ export class ActionSelector implements CognitiveEngine {
318
528
  salience: 0.6,
319
529
  payload: { schema: winner.affordance.schema, activation: winner.activation, entropy, tick },
320
530
  })
531
+
321
532
  if( deliberate )
322
533
  this._bus.publish({
323
534
  type: 'agency.selection.ambiguous',
@@ -329,6 +540,7 @@ export class ActionSelector implements CognitiveEngine {
329
540
  entropy, stakes: stk, tick,
330
541
  },
331
542
  })
543
+
332
544
  if( preemptedFrom )
333
545
  this._bus.publish({
334
546
  type: 'agency.action.preempted',
@@ -339,12 +551,16 @@ export class ActionSelector implements CognitiveEngine {
339
551
  })
340
552
  }
341
553
  catch( err ){
342
- logger.warn( `[selector] bus publish failed: ${ err instanceof Error ? err.message : String( err ) }` )
554
+ logger.warn(`[selector] bus publish failed: ${ err instanceof Error ? err.message : String( err ) }`)
343
555
  }
344
556
  }
345
557
 
346
558
  const commands: StateCommands = {
347
- set: [ intent ],
559
+ // A composite preemption rides along as a tombstone (never a delete — the
560
+ // executor's in-tick macro-advance would resurrect the parent).
561
+ set: compositeTombstone
562
+ ? [ intent, revocationEntity( compositeTombstone, compositeFrom ?? '', rupture, tick ) ]
563
+ : [ intent ],
348
564
  ...( preemptDelete ? { delete: [ preemptDelete ] } : {} ),
349
565
  metrics: [
350
566
  [ 'agency.field.eligible', eligible.length ],
@@ -354,8 +570,10 @@ export class ActionSelector implements CognitiveEngine {
354
570
  [ 'agency.selection.deliberate', deliberate ? 1 : 0 ],
355
571
  [ 'agency.selection.preempted', preemptedFrom ? 1 : 0 ],
356
572
  [ 'agency.selection.activation', winner.activation ],
573
+ ...stabMetrics,
357
574
  ],
358
575
  }
576
+
359
577
  return { commands }
360
578
  }
361
579
  }
@@ -373,10 +591,77 @@ export class ActionSelector implements CognitiveEngine {
373
591
  * TaskSwitcher uses internally. One disposition, two owners, native scales.
374
592
  */
375
593
  function effectiveSwitchCost( state: ReadonlySimulationState ): number {
376
- const params = readEffectiveParams( state, 'engine-config-action-selector' )
594
+ const params = readEffectiveParams( state, 'engine-config-action-selector')
377
595
  const base = num( params['switchCost'], BASE_SWITCH_COST )
378
596
  const focusTicks = metric( state, 'task_switch.current_focus_ticks', 0 )
379
- return base * ( 1 + focusTicks * FOCUS_GAIN )
597
+ // EXAFFERENCE P3 focus only hardens under a stable situation. `situation.stability`
598
+ // (absent ⇒ 1 ⇒ pre-P3 behavior) scales the focus-hardening term, so a Will in a
599
+ // destabilized world can't cling to a long-held focus. The fast, same-tick softener
600
+ // is `(1 - rupture)` applied at the call site; this is the slow, persistent one.
601
+ const stability = metric( state, 'situation.stability', 1 )
602
+ return base * ( 1 + focusTicks * FOCUS_GAIN * stability )
603
+ }
604
+
605
+ /**
606
+ * EXAFFERENCE P3 — the exafferent-rupture scalar. Max salience among the
607
+ * `provenance:'exafferent'` percepts fresh within `RUPTURE_WINDOW_TICKS`,
608
+ * mapped through `RUPTURE_SALIENCE_GATE` into [0,1]. Pure read of frozen state;
609
+ * `reafferent` percepts (our own echo, P2) are excluded by construction, so the
610
+ * mind is never ruptured by itself.
611
+ */
612
+ function computeRupture(
613
+ state: ReadonlySimulationState,
614
+ tick: Tick,
615
+ senseEvents: ReadonlyArray<{ salience: number; text?: string }> = [],
616
+ ): number {
617
+ let maxSalience = 0
618
+ for( const e of state.entities.values() ){
619
+ if( e.type !== 'percept') continue
620
+ const m = e.metadata
621
+ if( str( m?.['provenance'] ) !== 'exafferent') continue
622
+ const pTick = num( m?.['tick'], -1 )
623
+ if( pTick < 0 || tick - pTick > RUPTURE_WINDOW_TICKS ) continue
624
+ const s = num( m?.['salience'], 0 )
625
+ if( s > maxSalience ) maxSalience = s
626
+ }
627
+
628
+ // ACP §2b — buffered sense-channel percepts (bus-only; never entities). The
629
+ // echo guard extends to this path: an event whose text matches a live
630
+ // consequence descriptor is our own action coming back and cannot rupture.
631
+ if( senseEvents.length > 0 ){
632
+ const live = liveConsequences( state.entities, tick )
633
+ for( const ev of senseEvents ){
634
+ if( ev.text !== undefined && live.length > 0 && matchConsequenceText( live, ev.text ) ) continue
635
+ if( ev.salience > maxSalience ) maxSalience = ev.salience
636
+ }
637
+ }
638
+
639
+ if( maxSalience <= RUPTURE_SALIENCE_GATE ) return 0
640
+ return clamp01( ( maxSalience - RUPTURE_SALIENCE_GATE ) / ( 1 - RUPTURE_SALIENCE_GATE ) )
641
+ }
642
+
643
+ /**
644
+ * POLICY_REAFFERENCE P3 — schemas hit by a CLASS-final policy refusal visible in
645
+ * frozen state this tick (a refused `agency.outcome` lives exactly one tick).
646
+ *
647
+ * A refusal is an `agency.outcome`, never a `percept`, so it can NEVER feed
648
+ * computeRupture: the mind cannot rupture itself with its own boundary. This is
649
+ * a SEPARATE, explicit trigger — "the boundary just declared this forbidden, let
650
+ * go of any commitment I'm still weighing toward it" — distinct from a
651
+ * world-surprise exafferent rupture, and it carries its own revocation reason.
652
+ * Only `class` finality qualifies: an `instance` refusal means "not with those
653
+ * parameters", not "never", so a still-deliberating attempt may yet succeed.
654
+ */
655
+ function refusedClassSchemas( state: ReadonlySimulationState ): Set<string> {
656
+ const out = new Set<string>()
657
+ for( const e of state.entities.values() ){
658
+ if( e.type !== 'agency.outcome') continue
659
+ const m = e.metadata
660
+ if( m?.['refused'] !== true || str( m?.['finality'] ) !== 'class') continue
661
+ const schema = str( m?.['schema'] )
662
+ if( schema ) out.add( schema )
663
+ }
664
+ return out
380
665
  }
381
666
 
382
667
  /**
@@ -386,7 +671,7 @@ function effectiveSwitchCost( state: ReadonlySimulationState ): number {
386
671
  * weights have no clean single-trait owner, so they stay fixed (see the TODO catalogue).
387
672
  */
388
673
  function effectiveWeights( state: ReadonlySimulationState ): ScoreWeights {
389
- const p = readEffectiveParams( state, 'engine-config-action-selector' )
674
+ const p = readEffectiveParams( state, 'engine-config-action-selector')
390
675
  return {
391
676
  ...DEFAULT_WEIGHTS,
392
677
  risk: Math.max( 0, num( p['riskWeight'], DEFAULT_WEIGHTS.risk ) ),
@@ -404,10 +689,10 @@ function buildBias( state: ReadonlySimulationState ): BiasContext {
404
689
  let maxGoalPriority = 0
405
690
 
406
691
  for( const e of state.entities.values() ){
407
- if( e.type !== 'goal' ) continue
692
+ if( e.type !== 'goal') continue
408
693
  const m = e.metadata
409
694
  const status = str( m?.['status'] )
410
- if( status !== 'active' && status !== 'in_progress' ) continue
695
+ if( status !== 'active' && status !== 'in_progress') continue
411
696
  maxGoalPriority = Math.max( maxGoalPriority, num( m?.['priority'], 0 ) )
412
697
  }
413
698
 
@@ -441,7 +726,7 @@ function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<st
441
726
  cost: num( meta['cost'], 0 ),
442
727
  habitStrength: num( meta['habitStrength'], 0 ),
443
728
  available: meta['available'] === true,
444
- tags: Array.isArray( meta['tags'] ) ? ( meta['tags'] as unknown[] ).filter( ( t ): t is string => typeof t === 'string' ) : [],
729
+ tags: Array.isArray( meta['tags'] ) ? ( meta['tags'] as unknown[] ).filter( ( t ): t is string => typeof t === 'string') : [],
445
730
  planBias: typeof meta['planBias'] === 'number' ? ( meta['planBias'] as number ) : undefined,
446
731
  planId: str( meta['planId'] ),
447
732
  stepId: str( meta['stepId'] ),