@mindot/will 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +87 -22
  2. package/dist/channels/discord.d.ts +67 -6
  3. package/dist/channels/discord.js +112 -6
  4. package/dist/channels/discord.js.map +1 -1
  5. package/dist/channels/whatsapp.d.ts +1 -1
  6. package/dist/channels/whatsapp.js +4 -1
  7. package/dist/channels/whatsapp.js.map +1 -1
  8. package/dist/cli.js +13733 -11057
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +3729 -1199
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/effectors.d.ts +1 -1
  14. package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
  15. package/package.json +1 -1
  16. package/src/channels/discord.ts +189 -11
  17. package/src/channels/types.ts +90 -0
  18. package/src/channels/whatsapp.ts +13 -4
  19. package/src/cli.ts +9 -4
  20. package/src/cognition/agency/consequence.ts +122 -1
  21. package/src/cognition/agency/conversation.aim.ts +260 -0
  22. package/src/cognition/agency/engines/action.selector.ts +85 -3
  23. package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
  24. package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
  25. package/src/cognition/agency/engines/reafference.engine.ts +129 -2
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +16 -2
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/repertoire.ts +12 -5
  30. package/src/cognition/agency/selection.scoring.ts +33 -0
  31. package/src/cognition/agency/types.ts +35 -0
  32. package/src/cognition/cache/composition.ts +232 -0
  33. package/src/cognition/cache/deliberation.cache.ts +219 -0
  34. package/src/cognition/cache/fingerprint.ts +120 -0
  35. package/src/cognition/cache/types.ts +105 -0
  36. package/src/cognition/config.mirror.entities.ts +109 -1
  37. package/src/cognition/event.schemas.ts +22 -0
  38. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  39. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  40. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  41. package/src/cognition/faculties/executive.engine/context.ts +67 -13
  42. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  43. package/src/cognition/faculties/executive.engine/engine.ts +686 -187
  44. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
  45. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
  46. package/src/cognition/faculties/executive.engine/facet.ts +89 -26
  47. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  48. package/src/cognition/faculties/executive.engine/parser.ts +21 -1
  49. package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
  50. package/src/cognition/faculties/executive.engine/types.ts +69 -0
  51. package/src/cognition/faculties/goal.manager.ts +94 -14
  52. package/src/cognition/faculties/known.entity.tracker.ts +267 -28
  53. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  54. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  55. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  56. package/src/cognition/faculties/self.model.updater.ts +19 -12
  57. package/src/cognition/faculties/social.perception.ts +47 -3
  58. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  59. package/src/cognition/faculties/working.memory.ts +10 -20
  60. package/src/cognition/identity.entity.ts +205 -0
  61. package/src/cognition/index.ts +11 -0
  62. package/src/cognition/memory/vector.adapter.ts +12 -3
  63. package/src/cognition/memory/vector.embedder.ts +54 -7
  64. package/src/cognition/persona.prior.ts +6 -0
  65. package/src/cognition/senses/audition.engine/engine.ts +404 -46
  66. package/src/cognition/senses/base.sense.engine.ts +1 -1
  67. package/src/cognition/senses/index.ts +12 -0
  68. package/src/cognition/social.identity.ts +273 -0
  69. package/src/cognition/utilities/token.tracker.ts +246 -98
  70. package/src/core/orchestrator.ts +38 -0
  71. package/src/host/boot.ts +78 -22
  72. package/src/index.ts +35 -0
  73. package/src/llm/index.ts +415 -97
  74. package/src/llm/routing.ts +204 -0
  75. package/src/llm/summarizer.ts +5 -1
  76. package/src/llm/wire.contracts.ts +19 -0
  77. package/src/pma/index.ts +67 -53
  78. package/src/runners/thin-shim.runner.ts +18 -6
  79. package/src/sdk/will.ts +121 -22
  80. package/src/stem/assembly.audit.ts +1 -0
  81. package/src/stem/guards/identity.coherence.ts +17 -6
  82. package/src/stem/index.ts +82 -5
  83. package/src/stem/mind.ts +327 -79
  84. package/src/stem/policy/arbiter.ts +49 -14
  85. package/src/stem/policy/rule.table.ts +2 -2
  86. package/src/stem/tracts/effector.controller.ts +56 -9
  87. package/src/stem/tracts/outbox.writer.ts +40 -2
  88. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
@@ -71,6 +71,11 @@ interface Intent {
71
71
  */
72
72
  planId?: string
73
73
  planStepId?: string
74
+ /**
75
+ * The entity that evoked this — for an ideomotor winner, the `ideomotor.intent`
76
+ * the executive wrote. Read so enaction can retire it (see _dischargeWill).
77
+ */
78
+ evokedBy?: string
74
79
  }
75
80
 
76
81
  /** Authors the words for a self-initiated communicate the agency selected (no inbound triggered it). */
@@ -88,6 +93,22 @@ export class MotorSchemaExecutor implements CognitiveEngine {
88
93
  private _grants: AccessGrants | null = null
89
94
  private _bus: CognitiveBus | null = null
90
95
 
96
+ /**
97
+ * Two-phase outreach authoring. A facet cannot be awaited from inside a tick:
98
+ * `ExecutiveFacet.report()` only QUEUES in tick-discipline mode and the reasoning
99
+ * launches from `pump()`, which the ExecutiveEngine calls once per tick — so an
100
+ * in-tick `await` blocks the very loop that would produce the answer. Observed
101
+ * live: a 61s freeze of the whole mind inside one tick, then an empty result.
102
+ * (It passes unit tests because bare facets have no inbox and author inline.)
103
+ *
104
+ * So `_deliver` REQUESTS words and returns false (the intent holds 'awaiting'),
105
+ * the facet answers off-tick, and a later tick delivers. Process-local by design:
106
+ * an authoring call in flight cannot survive a restart, and the intent would
107
+ * simply re-request.
108
+ */
109
+ private _authoring = new Set<string>()
110
+ private _authored = new Map<string, string[]>()
111
+
91
112
  constructor( schemas: MotorSchema[] = INNATE_SCHEMAS ){
92
113
  for( const s of schemas ) this._schemas.set( s.id, s )
93
114
  }
@@ -173,18 +194,76 @@ export class MotorSchemaExecutor implements CognitiveEngine {
173
194
  else if( parentId && revoked.has( parentId ) ) del.push( id )
174
195
  }
175
196
 
197
+ // ── Words that landed off-tick → deliver now ─────────────────
198
+ // Second half of two-phase authoring: a previous tick asked a facet for the words
199
+ // and held the intent 'awaiting'; the facet answered between ticks. Runs BEFORE
200
+ // the timeout sweep so words arriving on the same tick the clock would expire are
201
+ // still spoken rather than discarded.
202
+ const spokeThisTick = new Set<string>()
203
+ for( const [ id, e ] of state.entities ){
204
+ if( e.type !== 'agency.intent' || str( e.metadata?.['status'] ) !== 'awaiting') continue
205
+ if( !this._authored.has( id ) ) continue
206
+
207
+ const intent = readIntent( id, e.metadata )
208
+ const predicted: EfferenceCopy = {
209
+ expectedReward: num( e.metadata?.['predictedReward'], intent.expectedReward ),
210
+ expectedValence: num( e.metadata?.['predictedValence'], intent.expectedValence ),
211
+ }
212
+ if( await this._deliver( id, intent, predicted, state, tick, set, del, metrics ) )
213
+ spokeThisTick.add( id )
214
+ }
215
+
216
+ // Words whose intent no longer exists (revoked, timed out, superseded) are words
217
+ // that will never be said — drop them so the map cannot grow without bound.
218
+ for( const id of this._authored.keys() )
219
+ if( !state.entities.has( id ) ) this._authored.delete( id )
220
+
176
221
  // ── Timeout stranded async intents ───────────────────────────
177
222
  // An 'awaiting' intent whose host/delivery never returned would block the
178
223
  // serial Will forever. After AWAIT_TIMEOUT ticks, abandon it as a failed
179
224
  // outcome (which also teaches reafference the action is unreliable here).
180
225
  for( const [ id, e ] of state.entities ){
181
226
  if( e.type !== 'agency.intent' || str( e.metadata?.['status'] ) !== 'awaiting') continue
227
+ // Already delivered above — its outcome and deletion are queued for this tick.
228
+ if( spokeThisTick.has( id ) ) continue
229
+ // Words are being authored right now. The clock PAUSES rather than extends: a
230
+ // facet LLM call is 10–30s of latency we do not control, and any fixed budget
231
+ // large enough to cover a slow one is also large enough to strand a dead one.
232
+ // While a call is genuinely in flight the world has not failed to answer. The
233
+ // pause is bounded, not open-ended: `authorOutreach` always settles (its own 60s
234
+ // timeout resolves empty), so `_authoring` always clears and the clock resumes.
235
+ if( this._authoring.has( id ) ) continue
182
236
  // POLICY_REAFFERENCE P4 — an escalated intent is HELD: the stem owns its
183
237
  // lifecycle (extended TTL → approve/deny/expire), so the executor must not
184
238
  // time it out at AWAIT_TIMEOUT and reconcile it as a phantom failure.
185
239
  if( e.metadata?.['escalated'] === true ) continue
186
240
  const dispatchedAt = num( e.metadata?.['dispatchedAt'], tick )
187
- if( tick - dispatchedAt < AWAIT_TIMEOUT ) continue
241
+ const age = tick - dispatchedAt
242
+
243
+ // Dispatched LATER than now ⇒ it was in flight when the mind went to sleep.
244
+ // Intents snapshot with the state and the tick counter restarts at 1 on wake,
245
+ // so a restored `awaiting` intent has a NEGATIVE age — and every guard here
246
+ // inverts on it: `age < AWAIT_TIMEOUT` is trivially true for -589, so it never
247
+ // timed out, and the selector's staleness went negative, INFLATING the
248
+ // incumbent's strength instead of decaying it. Measured on a live Will:
249
+ // incumbent 9.742 against challengers of 0.52, unpreemptable and immortal.
250
+ // One person's stale intent held the channel and every attempt to contact
251
+ // anyone else was refused, indefinitely, across restarts.
252
+ //
253
+ // Cleared WITHOUT a failure outcome: hibernating is not the world declining
254
+ // to answer. Reconciling it as a timeout would teach reafference that
255
+ // reaching that person does not work, which is a lesson about the process
256
+ // lifecycle, not about them.
257
+ if( age < 0 ){
258
+ del.push( id )
259
+ logger.info(
260
+ `[motor] cleared "${ str( e.metadata?.['schema'] ) ?? 'intent' }" left awaiting ` +
261
+ `across a restart (dispatched at tick ${ dispatchedAt }, now ${ tick })`
262
+ )
263
+ continue
264
+ }
265
+
266
+ if( age < AWAIT_TIMEOUT ) continue
188
267
 
189
268
  const intent = readIntent( id, e.metadata )
190
269
  const predicted: EfferenceCopy = {
@@ -437,17 +516,16 @@ export class MotorSchemaExecutor implements CognitiveEngine {
437
516
  return true
438
517
  }
439
518
 
440
- // Content authored upstream when present (host / host-facet); otherwise, for a
441
- // self-initiated communicate the agency just selected, author it now via a facet
442
- // the unified conversation voice, since no inbound triggered it. Empty ⇒ await.
519
+ // Content authored upstream when present (host / host-facet), else words a facet
520
+ // authored off-tick and landed since a previous tick asked for them. Neither
521
+ // request authoring (never awaited here see `_authoring`) and hold 'awaiting'.
443
522
  const authored = str( intent.parameters['content'] ) ?? firstMessage( intent.parameters['messages'] )
444
- let bubbles: string[] = authored ? [ authored ] : []
445
- if( bubbles.length === 0 && this._author ){
446
- const name = str( intent.parameters['targetEntityName'] ) ?? intent.targetEntityId ?? 'them'
447
- try { bubbles = await this._author.authorOutreach( intent.targetEntityId ?? '', name, str( intent.parameters['gist'] ) ) }
448
- catch( err ){ logger.warn(`[motor] outreach authoring failed: ${ errMsg( err ) }`) }
523
+ let bubbles: string[] = authored ? [ authored ] : ( this._authored.get( id ) ?? [] )
524
+ this._authored.delete( id )
525
+ if( bubbles.length === 0 ){
526
+ this._requestAuthoring( id, intent )
527
+ return false // nothing to send yet await
449
528
  }
450
- if( bubbles.length === 0 ) return false // nothing authored to send → await
451
529
 
452
530
  const request: ActionRequest = {
453
531
  effector,
@@ -477,6 +555,16 @@ export class MotorSchemaExecutor implements CognitiveEngine {
477
555
  text: bubbles.join('\n'),
478
556
  expiresAt: tick + CONSEQUENCE_TTL_TICKS, tick,
479
557
  }) )
558
+ // The words are out. Whatever willed them is DONE being an intention.
559
+ //
560
+ // Discharged here rather than at dispatch on purpose: `_deliver` holds the
561
+ // intent 'awaiting' while a facet authors, and that authoring can time out or
562
+ // return nothing. Clearing the will at dispatch would lose the intention
563
+ // silently — the mind would have decided to say something and simply never
564
+ // have, which is the failure `commands.ts` calls "the whole intention
565
+ // evaporated without a trace". Enaction is the discharge; a request is not.
566
+ if( result.success ) this._dischargeWill( intent, del )
567
+
480
568
  this._emitEnacted( intent, out, predicted, tick )
481
569
  this._emitActionOutcome( intent, result.success, result.feedback.outcomeQuality,
482
570
  clamp01( Math.abs( predicted.expectedReward - result.feedback.outcomeQuality ) ), tick )
@@ -484,6 +572,27 @@ export class MotorSchemaExecutor implements CognitiveEngine {
484
572
  return true
485
573
  }
486
574
 
575
+ /**
576
+ * Ask a facet for the words, off-tick. Fire-and-forget on purpose: awaiting this
577
+ * from inside `react()` deadlocks the tick loop against the facet pump. Idempotent
578
+ * per intent — a request already in flight is not duplicated, so the intent may sit
579
+ * 'awaiting' across many ticks with exactly one LLM call behind it.
580
+ */
581
+ private _requestAuthoring( id: string, intent: Intent ): void {
582
+ if( !this._author || this._authoring.has( id ) ) return
583
+
584
+ const name = str( intent.parameters['targetEntityName'] ) ?? intent.targetEntityId ?? 'them'
585
+ this._authoring.add( id )
586
+ void this._author
587
+ .authorOutreach( intent.targetEntityId ?? '', name, str( intent.parameters['gist'] ) )
588
+ .then( bubbles => {
589
+ if( bubbles.length > 0 ) this._authored.set( id, bubbles )
590
+ else logger.warn(`[motor] outreach authoring returned nothing for "${ intent.schema }"`)
591
+ } )
592
+ .catch( err => { logger.warn(`[motor] outreach authoring failed: ${ errMsg( err ) }`) } )
593
+ .finally( () => { this._authoring.delete( id ) } )
594
+ }
595
+
487
596
  // ── bus emission ─────────────────────────────────────────────
488
597
 
489
598
  private _emitEnacted( intent: Intent, enaction: Enaction, predicted: EfferenceCopy, tick: Tick ): void {
@@ -506,6 +615,38 @@ export class MotorSchemaExecutor implements CognitiveEngine {
506
615
  * RewardEvaluator reads it as a reward signal. `confidence` carries the agency's
507
616
  * own forward-model prior so calibration has a real prediction to score.
508
617
  */
618
+ /**
619
+ * Retire the `ideomotor.intent` that produced this act.
620
+ *
621
+ * Nothing deleted these. They were cleared only when the executive next ran and
622
+ * declined to name the same action again — and the executive runs on an interval,
623
+ * so between cycles a willed reach-out STOOD in state, was rebuilt into an
624
+ * affordance every single tick, and competed every single tick. Observed as
625
+ * dozens of identical lines:
626
+ *
627
+ * [selector] willed reach-out → … NOT selected: 0.297 < inspect… 0.340
628
+ *
629
+ * losing by four thousandths, over and over, until it won — twice. Fabrice got
630
+ * the same message byte-for-byte 25 ticks apart, two outbox ids.
631
+ *
632
+ * `justEnacted` was built to hold this line and cannot: it is a DECAYING
633
+ * quantity, capped at `repeatDamping` (0.30), and a standing intent outlasts it
634
+ * by construction. Damping a permanent pull only ever delays it. So the intention
635
+ * is discharged by being acted on, which is what an intention is — you meant to
636
+ * tell someone something, you told them, and it is finished. If the mind still
637
+ * wants to say more, the next executive cycle forms a new one, now seeing "I said
638
+ * this 25 ticks ago and have had no answer" in front of it.
639
+ *
640
+ * Satiation stays exactly as it was, and still earns its keep: it damps saying
641
+ * the same thing again for reasons that did NOT come from a standing will.
642
+ */
643
+ private _dischargeWill( intent: Intent, del: string[] ): void {
644
+ const willId = intent.evokedBy
645
+ if( !willId || !willId.startsWith('ideomotor-') ) return
646
+ del.push( willId )
647
+ logger.info(`[motor] discharged the will behind "${ intent.schema }" (${ willId })`)
648
+ }
649
+
509
650
  private _emitActionOutcome(
510
651
  intent: Intent, success: boolean, outcomeQuality: number, surprise: number, tick: Tick,
511
652
  ): void {
@@ -588,6 +729,7 @@ function readIntent( id: string, m: ReadonlyMap<string, unknown> | Record<string
588
729
  stepIndex: typeof meta['stepIndex'] === 'number' ? ( meta['stepIndex'] as number ) : undefined,
589
730
  planId: str( meta['planId'] ),
590
731
  planStepId: str( meta['stepId'] ),
732
+ evokedBy: str( meta['evokedBy'] ),
591
733
  }
592
734
  }
593
735
 
@@ -29,7 +29,12 @@ import type { CognitiveEngine, EngineResult } from '#cognition/types'
29
29
  import type { CognitiveEventSchema } from '#cognition/schema.registry'
30
30
  import type { SchemaRepertoire } from '#agency/schemas/repertoire'
31
31
  import { schemaEntityId, availabilityEntityId } from '#agency/schemas/repertoire'
32
+ import { asFinality } from '#stem/policy/arbiter'
32
33
  import { AWAIT_TIMEOUT } from '#agency/engines/motor.schema.executor'
34
+ import { readEffectiveParams } from '#cognition/persona.prior'
35
+ import {
36
+ SENT_TYPE, DEFAULT_REPLY_WINDOW_TICKS, resolveReplyExpectations,
37
+ } from '#agency/conversation.aim'
33
38
 
34
39
  const PROC_THRESHOLD = 0.60 // mirror of repertoire's threshold for the habitual-count metric
35
40
 
@@ -80,6 +85,11 @@ export class ReafferenceEngine implements CognitiveEngine {
80
85
  // action.outcome the PlanningEngine advances on. (The executor is the emitter
81
86
  // for sync/timeout outcomes; this is its async counterpart — one emitter each.)
82
87
  { type: 'action.outcome', version: 1, validate: () => null },
88
+ // Whether a person answers the mind when it speaks to them. Distinct from
89
+ // `interaction.occurred`, which reports something someone DID — a silence is
90
+ // not an act and so can never appear there, which is why "they never answer
91
+ // me" was unlearnable despite `socialStanding` being built to carry it.
92
+ { type: 'social.responsiveness', version: 1, validate: () => null },
83
93
  ]
84
94
  }
85
95
  /** Creation seam: register a composite proposed by the executive/deliberation facet. */
@@ -201,8 +211,17 @@ export class ReafferenceEngine implements CognitiveEngine {
201
211
  // is merely forbidden to do. The awaiting intent is still freed, and a
202
212
  // refused plan step is signalled unsuccessful so the plan doesn't hang.
203
213
  if( m['refused'] === true ){
204
- const finality = str( m['finality'] ) === 'class' ? 'class' : 'instance'
205
- this._repertoire.recordRefusal( schema, finality, tick )
214
+ const finality = asFinality( m['finality'] )
215
+
216
+ // P5 — 'context' means the refusal was NOT ABOUT THE ACTION (tainted
217
+ // context, an unavailable dependency, the arbiter itself down). Denting
218
+ // availability here would teach a lesson the policy never taught:
219
+ // "reach for this less", when nothing about the ability was the problem.
220
+ // So the intent is freed and the plan step signalled — the mind must not
221
+ // hang — and NOTHING else is written. Not competence, not availability.
222
+ if( finality !== 'context')
223
+ this._repertoire.recordRefusal( schema, finality, tick )
224
+
206
225
  if( fromState ) del.push( id )
207
226
  const refusedIntent = str( m['intentId'] )
208
227
  if( refusedIntent ) del.push( refusedIntent )
@@ -271,6 +290,19 @@ export class ReafferenceEngine implements CognitiveEngine {
271
290
  // Availability entries (P2) mirror the same way — empty until a refusal lands.
272
291
  for( const e of this._repertoire.availabilityEntities() ) set.push( e )
273
292
 
293
+ // ── 3.5 Did the words achieve what they were for? ────────────
294
+ // The motor loop above graded whether each act EXECUTED. For a communicative
295
+ // act that is the smaller half of the question: the outbox accepting a message
296
+ // is not the world answering it. Both were the same fact until now, which is
297
+ // how `reach-out` came to sit at 28 enactments / 28 successes while the person
298
+ // it kept reaching had said nothing back.
299
+ //
300
+ // Folded here rather than in a new engine because this is the same question
301
+ // this engine already exists to ask, one layer out — reafference over a social
302
+ // act instead of a motor one. The resolution itself is pure (conversation.aim);
303
+ // all that happens here is writing it down and saying it out loud.
304
+ const replied = this._resolveReplies( tick, state, set )
305
+
274
306
  // ── 4. Telemetry ─────────────────────────────────────────────
275
307
  const skills = this._repertoire.skills()
276
308
  const habitual = [ ...skills.values() ].filter( s => s.habitStrength >= PROC_THRESHOLD ).length
@@ -281,6 +313,10 @@ export class ReafferenceEngine implements CognitiveEngine {
281
313
  [ 'agency.habitual.count', habitual ],
282
314
  [ 'agency.sensory.confirmed', sensory ],
283
315
  )
316
+ // Quiet path stays byte-identical: a Will that has spoken to nobody, or whose
317
+ // every turn was answered, writes nothing here (cf. EXAFFERENCE P3).
318
+ if( replied.answered > 0 ) metrics.push([ 'social.answered.count', replied.answered ])
319
+ if( replied.unanswered > 0 ) metrics.push([ 'social.unanswered.count', replied.unanswered ])
284
320
  // Only emit the refusal metric when it fired — a never-refused Will writes
285
321
  // nothing here, preserving the byte-identical quiet path (cf. EXAFFERENCE P3).
286
322
  if( refused > 0 ) metrics.push([ 'agency.refused.count', refused ])
@@ -288,6 +324,97 @@ export class ReafferenceEngine implements CognitiveEngine {
288
324
  return { commands: { set, delete: del, metrics } }
289
325
  }
290
326
 
327
+ /**
328
+ * Latch each open turn's fate onto its own record and announce it once.
329
+ *
330
+ * Two rules earn their keep here:
331
+ *
332
+ * • MERGE, never replace. `StateManager.setEntity` overwrites the whole entity,
333
+ * and a `conversation.sent` carries `outboxMessageIds` — the sole key by which
334
+ * a later delivery ack can find it. Rewriting the record with only the fields
335
+ * this method cares about would sever that, silently, for every turn that got
336
+ * an answer.
337
+ *
338
+ * • Latch, don't recompute. `answeredAt`/`unansweredAt` persist, so the event
339
+ * fires on the one tick the fact changed rather than every tick for the rest
340
+ * of the session — which for an unanswered turn would be thousands of
341
+ * identical reputation hits against one person for one silence.
342
+ */
343
+ private _resolveReplies(
344
+ tick: Tick,
345
+ state: ReadonlySimulationState,
346
+ set: EntityInput[],
347
+ ): { answered: number; unanswered: number } {
348
+ // A trait, not a constant: how long a silence takes to mean something differs
349
+ // between minds, so it tunes through the persona prior like every other
350
+ // developable parameter. Absent ⇒ the default ⇒ the pre-seam behaviour.
351
+ //
352
+ // Read from the SELECTOR's config despite being applied here, because it and
353
+ // `repeatWindowTicks` are two readings of one disposition — how long this mind
354
+ // sits with something it has said. Held apart, a mind could tune itself into
355
+ // saying a thing again while still calling the silence too fresh to count.
356
+ const window = Math.max(
357
+ 1,
358
+ Math.round(
359
+ readEffectiveParams( state, 'engine-config-action-selector').replyWindowTicks
360
+ ?? DEFAULT_REPLY_WINDOW_TICKS
361
+ ),
362
+ )
363
+
364
+ const { answered, unanswered } = resolveReplyExpectations( state.entities, tick, window )
365
+ if( answered.length === 0 && unanswered.length === 0 ) return { answered: 0, unanswered: 0 }
366
+
367
+ const merge = ( id: string, patch: Record<string, unknown> ): void => {
368
+ const existing = state.entities.get( id )
369
+ if( !existing ) return
370
+ set.push({
371
+ id, type: SENT_TYPE,
372
+ metadata: { ...( existing.metadata as Record<string, unknown> ?? {} ), ...patch },
373
+ })
374
+ }
375
+
376
+ for( const { turn, at, with: said } of answered ){
377
+ // Their words, not merely the fact of them — see SpokenTurn.answeredWith.
378
+ merge( turn.entityId, { answeredAt: at, ...( said ? { answeredWith: said } : {} ) } )
379
+ this._emitResponsiveness( turn.targetEntityId, true, at - turn.tick, tick )
380
+ }
381
+
382
+ for( const turn of unanswered ){
383
+ merge( turn.entityId, { unansweredAt: tick } )
384
+ this._emitResponsiveness( turn.targetEntityId, false, tick - turn.tick, tick )
385
+ logger.info(
386
+ `[reafference] no answer from ${ turn.targetEntityName ?? turn.targetEntityId } ` +
387
+ `after ${ tick - turn.tick } ticks — "${ turn.preview.slice( 0, 60 ) }"`
388
+ )
389
+ }
390
+
391
+ return { answered: answered.length, unanswered: unanswered.length }
392
+ }
393
+
394
+ /**
395
+ * Publish how a person responded to being spoken to.
396
+ *
397
+ * Deliberately NOT an `interaction.occurred`: that event means "someone did
398
+ * something toward us" and carries a valence for what they did. A silence is
399
+ * nobody doing anything, and forcing it through that channel would have the
400
+ * ReputationTracker book a hostile *act* where there was only an absence — the
401
+ * mind would come to think it was being rebuffed rather than simply not
402
+ * answered yet. Separate signal, separate meaning, one consumer decides what
403
+ * either is worth.
404
+ */
405
+ private _emitResponsiveness( keid: string, answered: boolean, waitedTicks: number, tick: Tick ): void {
406
+ if( !this._bus ) return
407
+ try {
408
+ this._bus.publish({
409
+ type: 'social.responsiveness', version: 1, sourceEngine: this.name,
410
+ // An answer is ordinary; being ignored is the one worth interrupting for.
411
+ salience: answered ? 0.3 : 0.55,
412
+ payload: { keid, answered, waitedTicks, tick },
413
+ })
414
+ }
415
+ catch( err ){ logger.warn(`[reafference] responsiveness publish failed: ${ err instanceof Error ? err.message : String( err ) }`) }
416
+ }
417
+
291
418
  private _emitProceduralized( skill: LearnedSkill, tick: Tick ): void {
292
419
  if( !this._bus ) return
293
420
  try {
@@ -190,7 +190,17 @@ export class ProactiveCommunicator {
190
190
  // Generate outbox message IDs first so we can embed them in conversation.sent
191
191
  // and correlate deliveries back to this intent (TODO 11.3).
192
192
  const originalMessage = ( request.parameters?.originalMessage as string ) ?? ''
193
- const deliveryTick = ( request.parameters?.tick as number ) ?? 0
193
+ // `decidedAt` is the enacting tick and is always set by the caller; the
194
+ // `parameters.tick` fallback is for hosts that build a request by hand.
195
+ //
196
+ // This defaulted to 0 for every agency-path delivery, because MotorSchemaExecutor
197
+ // builds `parameters` from the intent and never put a tick in it. Two
198
+ // consequences, both silent: every `conversation.sent` was stamped tick 0, so
199
+ // anything comparing against it (satiation, undertaking discharge) compared
200
+ // against zero and never fired; and the id `conv-sent-<target>-0` was CONSTANT,
201
+ // so each send to a person overwrote the record of the last one — the mind kept
202
+ // exactly one memory of having spoken to each person, forever.
203
+ const deliveryTick = ( request.parameters?.tick as number ) ?? request.decidedAt ?? 0
194
204
  const replyToMessageId = ( request.parameters?.replyToMessageId as string ) ?? undefined
195
205
  const isAck = ( request.parameters?.isAck as boolean ) ?? false
196
206
 
@@ -264,9 +274,15 @@ export class ProactiveCommunicator {
264
274
  description: `I reach out to ${targetEntityName}: "${fullReply.slice( 0, 80 )}${fullReply.length > 80 ? '…' : ''}"`,
265
275
  commands,
266
276
  feedback: {
267
- outcomeQuality: 0.85,
277
+ // NOT a success yet. This reports the TRANSPORT, and the act's point is to be
278
+ // answered — an outcome not yet known at this moment. Scoring delivery as 0.85
279
+ // taught the mind that reaching out works every time, including the times it
280
+ // was ignored, which is why it re-asked the same question 19 times in two
281
+ // minutes. Neutral here; the unanswered/answered signal in outreach.silence.ts
282
+ // is what actually moves the pull toward this person.
283
+ outcomeQuality: 0.5,
268
284
  surprise: 0.15,
269
- lessons: [ `My message is queued for delivery to ${targetEntityName}.` ],
285
+ lessons: [ `My words are on their way to ${targetEntityName}. Whether they land is not yet known.` ],
270
286
  },
271
287
  }
272
288
  }
@@ -16,6 +16,7 @@
16
16
  // ─────────────────────────────────────────────────────────────
17
17
 
18
18
  import type { EntityInput, Tick } from '#core/types'
19
+ import type { DenialFinality, PolicyCounterfactual } from '#stem/policy/arbiter'
19
20
 
20
21
  export interface HostAckResult {
21
22
  success: boolean
@@ -31,7 +32,17 @@ export interface HostAckResult {
31
32
  * forbidden to do. `finality` decides how hard availability is cut.
32
33
  */
33
34
  refused?: boolean
34
- finality?: 'class' | 'instance'
35
+ finality?: DenialFinality
36
+ /**
37
+ * ENVELOPE_NARROWING P0 — what WOULD have been permitted, e.g.
38
+ * `{ field: 'amount', requested: 500, allowed: 100 }`.
39
+ *
40
+ * Carried onto the outcome so the tape and the thing the mind actually learns
41
+ * from agree about what happened. Until ENVELOPE_NARROWING P1 lands nothing
42
+ * READS it — that fold is blocked upstream on whether a scalar `allowed` is a
43
+ * ceiling or a floor — but dropping it here is what made the two disagree.
44
+ */
45
+ counterfactual?: PolicyCounterfactual
35
46
  }
36
47
 
37
48
  /**
@@ -79,7 +90,10 @@ export function reconcileInvocation(
79
90
  mode: 'external',
80
91
  tick,
81
92
  reconciled: true,
82
- ...( result.refused ? { refused: true, finality: result.finality ?? 'instance' } : {} ),
93
+ ...( result.refused ? { refused: true, finality: result.finality ?? 'parameter' } : {} ),
94
+ // Only when the arbiter actually reported a bound — a refusal without one
95
+ // writes no key at all, so the quiet path is unchanged.
96
+ ...( result.refused && result.counterfactual ? { counterfactual: result.counterfactual } : {} ),
83
97
  ...( provenance.planId ? { planId: provenance.planId } : {} ),
84
98
  ...( provenance.stepId ? { stepId: provenance.stepId } : {} ),
85
99
  },
@@ -0,0 +1,66 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/agency/restart.ts — the restart boundary
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // Two different things happen when a Will wakes from a snapshot, and conflating
6
+ // them is what made this a recurring class of bug rather than a single one:
7
+ //
8
+ // 1. TIME must not go backwards. Entities come back stamped with the tick they
9
+ // were written at, so the clock has to resume from the snapshot's tick
10
+ // rather than restart at 0 (WillStem.createWill does this via
11
+ // `clock.setTick`). Otherwise every `tick - stampedTick` in the codebase —
12
+ // 42 comparison sites across 19 files — computes a NEGATIVE age, and the
13
+ // guards built on them inverted spectacularly: an awaiting intent whose age
14
+ // read -589 could never time out, and the selector's staleness decay
15
+ // `1 - staleness × STALE_DECAY` became `1 + 19.6`, AMPLIFYING an incumbent
16
+ // from 0.47 to 9.74 and making it permanently unpreemptable.
17
+ //
18
+ // 2. Work that was IN FLIGHT does not resume. Resuming the clock fixes the
19
+ // arithmetic but says nothing about the semantics: an action dispatched
20
+ // moments before hibernation is not still dispatched hours later, and the
21
+ // window in which the world might have echoed our own words has closed.
22
+ // Leaving these behind is worse under a resumed clock than under a reset
23
+ // one, because they now look plausibly RECENT — an awaiting intent would be
24
+ // reconciled as a genuine timeout, teaching the mind that reaching that
25
+ // person does not work, when all that happened is that it slept.
26
+ //
27
+ // This module owns (2): what the agency considers in-flight, and therefore what
28
+ // must not cross the boundary. It is deliberately a short, explicit list rather
29
+ // than a heuristic — anything the agency stamps with a tick and expects to
30
+ // resolve within a few of them belongs here.
31
+ // ─────────────────────────────────────────────────────────────
32
+
33
+ import { CONSEQUENCE_TYPE } from '#agency/consequence'
34
+
35
+ /** Entity type of a committed action awaiting the world's answer. */
36
+ const INTENT_TYPE = 'agency.intent'
37
+
38
+ /**
39
+ * Ids of the restored entities that represent work in flight when the mind went
40
+ * to sleep. Pure — the caller drops them.
41
+ *
42
+ * • `agency.intent` with status 'awaiting' — dispatched, never answered. It is
43
+ * ABANDONED, not failed: sleeping is not the world declining to answer, and
44
+ * recording a failure here would teach reafference a lesson about the process
45
+ * lifecycle rather than about the action.
46
+ * • `agency.consequence` — the expected sensory footprint of an act, used both
47
+ * to recognise our own echo (P2) and to damp repeating ourselves (P5). Its
48
+ * TTL is a handful of ticks; across a sleep the echo will never arrive, and a
49
+ * stale one both mis-attributes genuine replies as self-caused and suppresses
50
+ * contact that should now be free to happen.
51
+ *
52
+ * A `selected` intent is deliberately NOT cleared: it has not been dispatched, so
53
+ * it is still an intention the mind holds rather than an action in flight.
54
+ */
55
+ export function inFlightOnRestore(
56
+ entities: ReadonlyMap<string, { type: string; metadata?: Record<string, unknown> }>,
57
+ ): string[] {
58
+ const drop: string[] = []
59
+
60
+ for( const [ id, e ] of entities ){
61
+ if( e.type === CONSEQUENCE_TYPE ){ drop.push( id ); continue }
62
+ if( e.type === INTENT_TYPE && e.metadata?.['status'] === 'awaiting') drop.push( id )
63
+ }
64
+
65
+ return drop
66
+ }
@@ -26,6 +26,7 @@
26
26
 
27
27
  import type { MotorSchema, LearnedSkill } from '#agency/types'
28
28
  import type { EntityInput, ReadonlySimulationState } from '#core/types'
29
+ import type { DenialFinality } from '#stem/policy/arbiter'
29
30
  import { INNATE_SCHEMAS } from '#agency/schemas/innate'
30
31
 
31
32
  const VALUE_ALPHA = 0.2 // value EMA rate
@@ -50,8 +51,8 @@ const DROP_HABIT = 0.05 // below this (and learned) the skill is forgott
50
51
  // availability never floors at zero, and it climbs back toward 1 with disuse of
51
52
  // the refusal. P2 keys availability by SCHEMA; per-(schema, params) envelope
52
53
  // narrowing is a follow-up that belongs at selection time, not fielding time.
53
- const AVAIL_DROP_CLASS = 0.50 // multiplicative cut on a class-final refusal
54
- const AVAIL_DROP_INSTANCE = 0.12 // lighter cut on an instance-final refusal
54
+ const AVAIL_DROP_CLASS = 0.50 // multiplicative cut on a class-final refusal
55
+ const AVAIL_DROP_PARAMETER = 0.12 // lighter cut when only the arguments were refused
55
56
  const AVAIL_FLOOR = 0.05 // never zero — re-probe must always be possible
56
57
  const AVAIL_RECOVERY = 0.02 // per-decay climb back toward 1
57
58
  const AVAIL_RECOVERED = 0.999 // at/above this the entry is dropped (quiet path)
@@ -119,13 +120,19 @@ export class SchemaRepertoire {
119
120
 
120
121
  /**
121
122
  * Fold a policy refusal into the availability layer (NOT competence). A
122
- * `class` refusal cuts availability hard; an `instance` refusal dents it
123
+ * `class` refusal cuts availability hard; a `parameter` refusal dents it
123
124
  * lightly. Multiplicative so repeated refusals compound toward — but never
124
125
  * reach — zero, keeping re-probe alive.
126
+ *
127
+ * `context` is EXCLUDED FROM THE SIGNATURE, not handled inside: a refusal
128
+ * that was not about the action must never reach the availability layer at
129
+ * all, and making that a type error rather than a convention means a future
130
+ * caller cannot quietly re-introduce the dent. The routing decision lives in
131
+ * the ReafferenceEngine's refused branch (P5).
125
132
  */
126
- recordRefusal( schema: string, finality: 'class' | 'instance', tick: number ): number {
133
+ recordRefusal( schema: string, finality: Exclude<DenialFinality, 'context'>, tick: number ): number {
127
134
  const prev = this._availability.get( schema )?.value ?? 1
128
- const drop = finality === 'class' ? AVAIL_DROP_CLASS : AVAIL_DROP_INSTANCE
135
+ const drop = finality === 'class' ? AVAIL_DROP_CLASS : AVAIL_DROP_PARAMETER
129
136
  const value = Math.max( AVAIL_FLOOR, prev * ( 1 - drop ) )
130
137
  this._availability.set( schema, { value, lastRefusedTick: tick } )
131
138
  return value
@@ -13,6 +13,8 @@
13
13
  // + driveUrgency
14
14
  // + habitStrength·W (additive habit bonus)
15
15
  // + planBias·W (top-down prior)
16
+ // + willBias·W (deliberate volition)
17
+ // + socialPrior·W (learned read on WHO)
16
18
  // − cost
17
19
  // − inhibition
18
20
  // − risk
@@ -47,9 +49,34 @@ export interface ScoreWeights {
47
49
  habit: number
48
50
  /** Top-down planning prior — how strongly an executing plan biases the competition. */
49
51
  plan: number
52
+ /**
53
+ * Volitional prior — how strongly a DELIBERATE executive decision biases the
54
+ * competition. Peer of `plan`: a plan's frontier is a standing prior, a willed act
55
+ * is a prior formed this cycle. Strong enough that no habit (≤1) can out-weigh a
56
+ * confident decision, weak enough that inhibition, threat, or a genuinely more
57
+ * pressing affordance still can — willing is not a rubber stamp.
58
+ */
59
+ will: number
60
+ /**
61
+ * How strongly the mind's LEARNED read on an addressee biases acting toward them.
62
+ * Peer of `goal` in magnitude: who someone has shown themselves to be should weigh
63
+ * about as much as a goal pointed at them. Signed — a person the mind has come to
64
+ * trust pulls it toward contact, one who never answers pulls it away — and it
65
+ * reaches zero for anyone unknown, so a mind that knows no one scores as before.
66
+ */
67
+ social: number
50
68
  cost: number
51
69
  inhib: number
52
70
  risk: number
71
+ /**
72
+ * How strongly an act's own live footprint damps doing it again (EXAFFERENCE
73
+ * P5). Satiation, not a lock: it decays with the descriptor, so the pull
74
+ * returns once the window in which the world could have answered has passed.
75
+ * Peer of `will` in magnitude — a freshly-delivered message should roughly
76
+ * cancel the volitional prior that produced it, and no more, so a genuinely
77
+ * pressing reason to speak again still wins.
78
+ */
79
+ repeat: number
53
80
  }
54
81
 
55
82
  export const DEFAULT_WEIGHTS: ScoreWeights = {
@@ -59,9 +86,12 @@ export const DEFAULT_WEIGHTS: ScoreWeights = {
59
86
  drive: 0.25,
60
87
  habit: 0.20,
61
88
  plan: 0.30,
89
+ will: 0.30,
90
+ social: 0.30,
62
91
  cost: 0.20,
63
92
  inhib: 0.30,
64
93
  risk: 0.20,
94
+ repeat: 0.30,
65
95
  }
66
96
 
67
97
  /**
@@ -154,9 +184,12 @@ export function scoreAffordance(
154
184
  + w.drive * driveUrgency( a, bias )
155
185
  + w.habit * a.habitStrength
156
186
  + w.plan * ( a.planBias ?? 0 )
187
+ + w.will * ( a.willBias ?? 0 )
188
+ + w.social * ( a.socialPrior ?? 0 )
157
189
  - w.cost * a.cost
158
190
  - w.inhib * bias.inhibition
159
191
  - w.risk * risk( a, bias )
192
+ - w.repeat * ( a.justEnacted ?? 0 )
160
193
  )
161
194
  // POLICY_REAFFERENCE P2 — policy availability damps a POSITIVE activation only,
162
195
  // never flipping its sign: a refused ability competes weakly (so it is rarely