@mindot/will 0.9.0 → 0.10.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 (110) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +16660 -15206
  4. package/dist/index.js.map +1 -1
  5. package/dist/{channels → surface/channels}/discord.d.ts +50 -3
  6. package/dist/{channels → surface/channels}/discord.js +102 -7
  7. package/dist/surface/channels/discord.js.map +1 -0
  8. package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
  9. package/dist/{channels → surface/channels}/whatsapp.js +7 -4
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +4406 -2904
  12. package/dist/surface/cli.js.map +1 -0
  13. package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
  14. package/dist/{mcp → surface/mcp}/effectors.js +10 -4
  15. package/dist/surface/mcp/effectors.js.map +1 -0
  16. package/dist/{will-DbDj_TEH.d.ts → will-evj9_vrd.d.ts} +5699 -4854
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +237 -3
  19. package/src/cognition/agency/conversation.aim.ts +32 -0
  20. package/src/cognition/agency/engines/action.selector.ts +59 -3
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +114 -10
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +362 -27
  24. package/src/cognition/agency/engines/reafference.engine.ts +43 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/reconcile.learning.ts +5 -4
  27. package/src/cognition/agency/schemas/innate.ts +96 -1
  28. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  29. package/src/cognition/agency/selection.scoring.ts +16 -0
  30. package/src/cognition/agency/settlement.ts +203 -0
  31. package/src/cognition/agency/types.ts +38 -1
  32. package/src/cognition/config.mirror.entities.ts +0 -1
  33. package/src/cognition/event.schemas.ts +0 -8
  34. package/src/cognition/faculties/affective.blender.ts +21 -4
  35. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  36. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  37. package/src/cognition/faculties/executive.engine/context.ts +41 -35
  38. package/src/cognition/faculties/executive.engine/engine.ts +270 -113
  39. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +126 -59
  40. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +10 -0
  41. package/src/cognition/faculties/executive.engine/parser.ts +100 -38
  42. package/src/cognition/faculties/executive.engine/prompt.factory.ts +168 -23
  43. package/src/cognition/faculties/executive.engine/types.ts +18 -15
  44. package/src/cognition/faculties/exteroception.ts +99 -98
  45. package/src/cognition/faculties/goal.manager.ts +50 -2
  46. package/src/cognition/faculties/known.entity.tracker.ts +22 -3
  47. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  48. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  49. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  50. package/src/cognition/faculties/working.memory.ts +6 -1
  51. package/src/cognition/instruction.handler.ts +1 -1
  52. package/src/cognition/memory/vector.embedder.ts +1 -3
  53. package/src/cognition/percept.entity.ts +122 -0
  54. package/src/cognition/sense.boundary.ts +176 -0
  55. package/src/cognition/senses/audition.engine/engine.ts +97 -16
  56. package/src/cognition/senses/base.sense.engine.ts +104 -6
  57. package/src/cognition/senses/index.ts +71 -13
  58. package/src/cognition/senses/provenance.ts +128 -0
  59. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  60. package/src/cognition/social.identity.ts +23 -0
  61. package/src/cognition/utilities/token.tracker.ts +36 -5
  62. package/src/core/orchestrator.ts +13 -0
  63. package/src/index.ts +39 -5
  64. package/src/llm/gate.ts +48 -0
  65. package/src/llm/index.ts +102 -25
  66. package/src/llm/wire.contracts.ts +38 -0
  67. package/src/pma/index.ts +1 -1
  68. package/src/stem/index.ts +92 -21
  69. package/src/stem/mind.ts +26 -2
  70. package/src/stem/policy/arbiter.ts +10 -0
  71. package/src/stem/profiles/built-in.ts +7 -0
  72. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  73. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  74. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  75. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  76. package/src/{profiles → stem/profiles}/index.ts +1 -1
  77. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  78. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  79. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  80. package/src/stem/tracts/effector/types.ts +110 -0
  81. package/src/stem/tracts/effector.controller.ts +112 -401
  82. package/src/stem/tracts/outbox.controller.ts +30 -17
  83. package/src/stem/tracts/sensory.controller.ts +6 -6
  84. package/src/stem/tracts/session.logger.ts +6 -1
  85. package/src/stem/tracts/transport/types.ts +20 -1
  86. package/src/stem/tracts/transport.controller.ts +33 -9
  87. package/src/{channels → surface/channels}/discord.ts +224 -7
  88. package/src/{channels → surface/channels}/roster.ts +1 -1
  89. package/src/{channels → surface/channels}/types.ts +1 -1
  90. package/src/{channels → surface/channels}/whatsapp.ts +9 -6
  91. package/src/{cli.ts → surface/cli.ts} +6 -6
  92. package/src/{host → surface/host}/boot.ts +3 -3
  93. package/src/{host → surface/host}/utterances.ts +2 -2
  94. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  95. package/src/{mcp → surface/mcp}/server.ts +19 -5
  96. package/src/{sdk → surface/sdk}/will.ts +156 -28
  97. package/src/{serve → surface/serve}/server.ts +11 -4
  98. package/src/types.ts +24 -3
  99. package/dist/channels/discord.js.map +0 -1
  100. package/dist/channels/whatsapp.js.map +0 -1
  101. package/dist/cli.js.map +0 -1
  102. package/dist/mcp/effectors.js.map +0 -1
  103. package/src/extensions/livestream.ext.ts +0 -570
  104. package/src/extensions/time.ext.ts +0 -339
  105. package/src/profiles/built-in.ts +0 -7
  106. package/src/runners/coherence.runner.ts +0 -49
  107. package/src/runners/outreach.runner.ts +0 -155
  108. package/src/runners/social.runner.ts +0 -179
  109. package/src/runners/thin-shim.runner.ts +0 -205
  110. /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { logger } from '#core/logger'
26
26
  import type {
27
- Duration, Tick, SimulationContext,
27
+ Duration, Tick, SimulationContext, SimulationEvent,
28
28
  ReadonlySimulationState, StateCommands, EntityInput,
29
29
  } from '#core/types'
30
30
  import type { CognitiveBus } from '#cognition/bus'
@@ -39,10 +39,21 @@ import type { ActionRequest, ActionResult } from '#types'
39
39
  import { INNATE_SCHEMAS } from '#agency/schemas/innate'
40
40
  import { enact, type Enaction } from '#agency/execution.primitives'
41
41
  import { revokedIntentIds, revocationId, staleRevocationIds } from '#agency/revocation'
42
+ import { addressesOf } from '#cognition/social.identity'
43
+ import { lastAnsweredByEntity } from '#agency/conversation.aim'
42
44
  import {
43
- CONSEQUENCE_TYPE, CONSEQUENCE_TTL_TICKS,
44
- consequenceEntity, fnv1a, paramsKey,
45
- } from '#agency/consequence'
45
+ CONSEQUENCE_TYPE, CONSEQUENCE_TTL_TICKS, ENACTED_TYPE,
46
+ consequenceEntity, enactedEntity, fnv1a, paramsKey, spokenAtByEntity } from '#agency/consequence'
47
+
48
+ /**
49
+ * How long a durable enaction record is kept. Generous against the satiation
50
+ * window (`repeatWindowTicks`, 60 by default) rather than equal to it, because
51
+ * the window is persona-tunable and read in the synthesizer, not here — a
52
+ * retention shorter than some tenant's window would silently reintroduce the
53
+ * very gap this record exists to close. Past this the satiation term is 0
54
+ * regardless, so keeping it longer buys nothing.
55
+ */
56
+ const ENACTED_RETENTION_TICKS = 600
46
57
 
47
58
  /** Ticks an async (communicate/external) intent may stay 'awaiting' before it is
48
59
  * abandoned. Exported: the ReafferenceEngine's sensory-confirmation path (P5)
@@ -78,9 +89,41 @@ interface Intent {
78
89
  evokedBy?: string
79
90
  }
80
91
 
92
+ /**
93
+ * What an authoring pass produced.
94
+ *
95
+ * `bubbles` empty is AMBIGUOUS on its own — it is also what a timed-out facet, a
96
+ * full facet budget, and a pass deferring to one already in flight all return.
97
+ * `answered` is what disambiguates it: whether a facet actually reasoned and came
98
+ * back. An empty answer IS an answer; an empty non-answer means the pass never
99
+ * happened and is worth asking for again.
100
+ */
101
+ export interface OutreachResult {
102
+ bubbles: string[]
103
+ /** The mind considered speaking and declared silence — the strongest answer. */
104
+ withheld?: boolean
105
+ /**
106
+ * A facet reasoned and its decision came back, whatever it held. Absent means
107
+ * no pass ran at all (no executive, budget full, deferring to one in flight,
108
+ * timed out, threw) — only THAT is worth asking again for.
109
+ *
110
+ * Without this the caller could not tell the two apart, so it re-asked on
111
+ * every tick. Observed live: nineteen authoring passes for one outreach, each
112
+ * returning an empty answer, none of them delivering a word, and the person
113
+ * she had decided to contact never heard from her.
114
+ */
115
+ answered?: boolean
116
+ }
117
+
81
118
  /** Authors the words for a self-initiated communicate the agency selected (no inbound triggered it). */
82
119
  export interface OutreachAuthor {
83
- authorOutreach( entityId: string, entityName: string, gist?: string ): Promise<string[]>
120
+ /** An array return is still honoured — it reads as "no words, and I am not saying why". */
121
+ authorOutreach( entityId: string, entityName: string, gist?: string ): Promise<string[] | OutreachResult>
122
+ /**
123
+ * True while a turn with this person is still resolving — they spoke and the
124
+ * reply has not landed yet. Optional: an author that cannot say is not blocking.
125
+ */
126
+ isSpeakingTo?( entityId: string ): boolean
84
127
  }
85
128
 
86
129
  export class MotorSchemaExecutor implements CognitiveEngine {
@@ -108,6 +151,30 @@ export class MotorSchemaExecutor implements CognitiveEngine {
108
151
  */
109
152
  private _authoring = new Set<string>()
110
153
  private _authored = new Map<string, string[]>()
154
+ /**
155
+ * Intents whose facet considered speaking and chose NOT to.
156
+ *
157
+ * A declined outreach used to resolve by rotting: no words arrived, so the
158
+ * intent sat 'awaiting' until AWAIT_TIMEOUT abandoned it as a FAILURE — and
159
+ * reafference folded that into `reach-out`'s competence. The mind was learning
160
+ * it is bad at speaking from the times it decided not to speak. Live, a COO
161
+ * declining correctly ("nothing new to add — a sixth message would repeat")
162
+ * took a competence hit for the judgement.
163
+ *
164
+ * Silence is an ANSWER, not the absence of one. Held here so the sweep can
165
+ * resolve it as what it is: the intent is freed, and nothing is taught about
166
+ * an ability that was never in question.
167
+ */
168
+ private _withheld = new Map<string, string>()
169
+
170
+ /**
171
+ * The tick at which each held intent's words were REQUESTED from a facet.
172
+ *
173
+ * Authoring is off-tick and can take 10–30s of real time. In that window the
174
+ * situation the words were composed for can move — and until now nothing
175
+ * looked. See `situationMoved`.
176
+ */
177
+ private _composedAt = new Map<string, Tick>()
111
178
 
112
179
  constructor( schemas: MotorSchema[] = INNATE_SCHEMAS ){
113
180
  for( const s of schemas ) this._schemas.set( s.id, s )
@@ -136,6 +203,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
136
203
  { type: 'agency.enacted', version: 1, validate: () => null },
137
204
  { type: 'agency.invocation', version: 1, validate: () => null },
138
205
  { type: 'agency.communicate', version: 1, validate: () => null },
206
+ { type: 'action.withheld', version: 1, validate: () => null },
139
207
  ]
140
208
  }
141
209
  subscribes(): string[] { return [] }
@@ -144,7 +212,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
144
212
  * The executor is plan-agnostic. A plan does NOT dispatch steps here — it biases
145
213
  * the affordance competition (see PLANNING_AS_PRIOR_TODO.md), so a plan-driven
146
214
  * action reaches the executor as an ordinary committed `agency.intent` the
147
- * selector won. That intent already carries planId/stepId provenance (stamped by
215
+ * selector won. That intent already carries its planId/stepId link (stamped by
148
216
  * the selector from the winning affordance); `_emitActionOutcome` threads it back
149
217
  * out, which is how the PlanningEngine advances. Nothing plan-specific here.
150
218
  */
@@ -161,6 +229,10 @@ export class MotorSchemaExecutor implements CognitiveEngine {
161
229
  const set: EntityInput[] = []
162
230
  const del: string[] = []
163
231
  const metrics: Array<[ string, number ]> = []
232
+ // Dispatch announcements bound for the SIMULATION bus — the only one the host
233
+ // hears. See _emitDispatch: the cognitive copy reaches the mind's own
234
+ // faculties and reaches no host at all.
235
+ const events: Array<Omit<SimulationEvent, 'id' | 'timestamp' | 'tick'>> = []
164
236
 
165
237
  const energy = state.metrics.get('energy.level') ?? 100
166
238
  const stress = state.metrics.get('stress.load' ) ?? 0
@@ -173,6 +245,22 @@ export class MotorSchemaExecutor implements CognitiveEngine {
173
245
  if( tick >= num( e.metadata?.['expiresAt'], 0 ) ) del.push( id )
174
246
  }
175
247
 
248
+ // ── Expire durable enaction records ──────────────────────────
249
+ // One per (schema, target), refreshed in place, so the live set is bounded
250
+ // by pairs actually enacted rather than by enactions — but a Will that meets
251
+ // many people would still accrete one per person per schema forever, and the
252
+ // soak test asserts entities plateau. Dropped once no satiation window could
253
+ // still be reading them: past ENACTED_RETENTION_TICKS the term is 0 anyway,
254
+ // so the record is indistinguishable from its own absence.
255
+ for( const [ id, e ] of state.entities ){
256
+ if( e.type !== ENACTED_TYPE ) continue
257
+ const at = num( e.metadata?.['tick'], 0 )
258
+ // A record stamped later than now is a restored one from a previous
259
+ // session (the tick counter restarts on wake) — same trap `liveConsequences`
260
+ // documents. Drop it rather than let it read as "just now" all session.
261
+ if( at > tick || tick - at > ENACTED_RETENTION_TICKS ) del.push( id )
262
+ }
263
+
176
264
  // ── Revocation tombstones (EXAFFERENCE P4) ───────────────────
177
265
  // Reap expired tombstones (intent vanished otherwise), and collect live ones
178
266
  // so the selected-processing loop can refuse the half-race case: Deliberation
@@ -217,6 +305,10 @@ export class MotorSchemaExecutor implements CognitiveEngine {
217
305
  // that will never be said — drop them so the map cannot grow without bound.
218
306
  for( const id of this._authored.keys() )
219
307
  if( !state.entities.has( id ) ) this._authored.delete( id )
308
+ for( const id of this._withheld.keys() )
309
+ if( !state.entities.has( id ) ) this._withheld.delete( id )
310
+ for( const id of this._composedAt.keys() )
311
+ if( !state.entities.has( id ) ) this._composedAt.delete( id )
220
312
 
221
313
  // ── Timeout stranded async intents ───────────────────────────
222
314
  // An 'awaiting' intent whose host/delivery never returned would block the
@@ -233,6 +325,48 @@ export class MotorSchemaExecutor implements CognitiveEngine {
233
325
  // pause is bounded, not open-ended: `authorOutreach` always settles (its own 60s
234
326
  // timeout resolves empty), so `_authoring` always clears and the clock resumes.
235
327
  if( this._authoring.has( id ) ) continue
328
+ // The facet was asked and chose silence. Resolve it as that rather than
329
+ // letting the clock abandon it as a failure — see `_withheld`.
330
+ if( this._withheld.has( id ) ){
331
+ const intent = readIntent( id, e.metadata )
332
+ const why = this._withheld.get( id ) ?? 'I considered speaking and chose not to.'
333
+ set.push({
334
+ id: `agency-outcome-${ tick }-${ id }`,
335
+ type: 'agency.outcome',
336
+ metadata: {
337
+ schema: intent.schema, intentId: id,
338
+ targetEntityId: intent.targetEntityId,
339
+ withheld: true,
340
+ description: why,
341
+ mode: 'communicate', tick,
342
+ ...( intent.planId ? { planId: intent.planId } : {} ),
343
+ ...( intent.planStepId ? { stepId: intent.planStepId } : {} ),
344
+ },
345
+ })
346
+ del.push( id )
347
+ this._withheld.delete( id )
348
+ metrics.push([ 'agency.communicate.withheld', 1 ])
349
+ // A DISTINCT event, never `action.outcome`: six faculties learn from that
350
+ // one's `success`, and a withheld turn published as `success: false`
351
+ // teaches the mind it is bad at speaking from the times it decided not to
352
+ // speak — the exact regression #123 exists to prevent. This one is read
353
+ // only by the executive's record of what it did.
354
+ if( this._bus ){
355
+ try {
356
+ this._bus.publish({
357
+ type: 'action.withheld', version: 1, sourceEngine: this.name,
358
+ salience: 0.4,
359
+ payload: {
360
+ actionType: intent.schema, targetEntityId: intent.targetEntityId,
361
+ description: why, tick,
362
+ ...( intent.planId ? { planId: intent.planId } : {} ),
363
+ },
364
+ })
365
+ }
366
+ catch { /* unregistered schema is telemetry-only */ }
367
+ }
368
+ continue
369
+ }
236
370
  // POLICY_REAFFERENCE P4 — an escalated intent is HELD: the stem owns its
237
371
  // lifecycle (extended TTL → approve/deny/expire), so the executor must not
238
372
  // time it out at AWAIT_TIMEOUT and reconcile it as a phantom failure.
@@ -278,7 +412,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
278
412
  del.push( id )
279
413
  this._emitEnacted( intent, timedOut, predicted, tick )
280
414
  if( intent.planId && intent.planStepId )
281
- this._emitActionOutcome( intent, false, 0, 1, tick )
415
+ this._emitActionOutcome( intent, false, 0, 1, tick,
416
+ `No answer came back — I gave up waiting after ${ tick - dispatchedAt } ticks.` )
282
417
  logger.info(`[motor] ⏱ "${ intent.schema }" timed out after ${ tick - dispatchedAt } ticks`)
283
418
  }
284
419
 
@@ -321,7 +456,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
321
456
  enactedCount++
322
457
  this._emitEnacted( intent, enaction, predicted, tick )
323
458
  this._emitActionOutcome( intent, enaction.success, enaction.outcomeQuality,
324
- clamp01( Math.abs( predicted.expectedReward - enaction.outcomeQuality ) ), tick )
459
+ clamp01( Math.abs( predicted.expectedReward - enaction.outcomeQuality ) ), tick,
460
+ enaction.description )
325
461
 
326
462
  // If this was a macro step, advance (or finalize) its parent.
327
463
  if( intent.parentIntentId )
@@ -360,6 +496,13 @@ export class MotorSchemaExecutor implements CognitiveEngine {
360
496
  set.push( consequenceEntity({
361
497
  intentId: id, schema: intent.schema,
362
498
  mode: enaction.mode === 'communicate' ? 'communicate' : 'external',
499
+ // A communicate with no words yet has not happened. The footprint is
500
+ // still written (P1/P2 want it the moment the words land), but it must
501
+ // not satiate — attempting to speak is not speaking. See
502
+ // ConsequenceDescriptor.pending. An EXTERNAL dispatch IS the act: the
503
+ // host is doing it now, and not asking twice while waiting is exactly
504
+ // what satiation is for.
505
+ ...( enaction.mode === 'communicate' && !awaitingText ? { pending: true } : {} ),
363
506
  ...( enaction.mode === 'communicate'
364
507
  ? { effector: COMM_SCHEMA_TO_EFFECTOR[ intent.schema ] ?? intent.schema }
365
508
  : {} ),
@@ -368,7 +511,16 @@ export class MotorSchemaExecutor implements CognitiveEngine {
368
511
  paramsHash: fnv1a( paramsKey( intent.parameters ) ),
369
512
  expiresAt: tick + CONSEQUENCE_TTL_TICKS, tick,
370
513
  }) )
371
- this._emitDispatch( intent, enaction.mode, tick )
514
+ // The durable half, for an act with an object that is not speech.
515
+ // Speech has `conversation.sent` and objectless acts have
516
+ // `LearnedSkill.lastEnactedTick`; this is the peer those two left out,
517
+ // and without it satiation expired with the descriptor at the ECHO
518
+ // window rather than lasting the satiation window. Written at the same
519
+ // moment as the descriptor — the dispatch IS the act for an external
520
+ // effector — and keyed per (schema, target) so it refreshes in place.
521
+ if( intent.targetEntityId && enaction.mode !== 'communicate')
522
+ set.push( enactedEntity( intent.schema, intent.targetEntityId, tick ) )
523
+ events.push( ...this._emitDispatch( intent, enaction.mode, tick, state ) )
372
524
  metrics.push([ enaction.mode === 'communicate'
373
525
  ? 'agency.communicate.dispatched'
374
526
  : 'agency.invocation.dispatched', 1 ])
@@ -377,7 +529,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
377
529
  }
378
530
 
379
531
  metrics.push([ 'agency.executor.enacted', enactedCount ])
380
- return { commands: { set, delete: del, metrics } }
532
+ return { commands: { set, delete: del, metrics }, ...( events.length > 0 ? { events } : {} ) }
381
533
  }
382
534
 
383
535
  // ── composite machinery ──────────────────────────────────────
@@ -453,7 +605,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
453
605
  this._emitEnacted( compIntent, compEnaction,
454
606
  { expectedReward: compIntent.expectedReward, expectedValence: compIntent.expectedValence }, tick )
455
607
  this._emitActionOutcome( compIntent, true, avgQuality,
456
- clamp01( Math.abs( compIntent.expectedReward - avgQuality ) ), tick )
608
+ clamp01( Math.abs( compIntent.expectedReward - avgQuality ) ), tick,
609
+ compEnaction.description )
457
610
  }
458
611
 
459
612
  private _subIntent(
@@ -516,17 +669,63 @@ export class MotorSchemaExecutor implements CognitiveEngine {
516
669
  return true
517
670
  }
518
671
 
672
+ // ── Am I mid-sentence with them? ─────────────────────────────
673
+ // A reply and a self-initiated message are two paths to the same person, and
674
+ // neither can see the other. Observed live: a conversation facet answered a
675
+ // question and the agency delivered a proactive message to the same human in
676
+ // the SAME millisecond — one mind speaking to one person twice at once.
677
+ //
678
+ // Holding costs a tick and the words keep. If what they just said changes
679
+ // what I meant to say, `situationMoved` catches that on the way back round,
680
+ // which is the outcome we want anyway.
681
+ if( intent.targetEntityId && this._author?.isSpeakingTo?.( intent.targetEntityId ) ){
682
+ logger.debug(`[motor] holding "${ intent.schema }" — a turn with ${ intent.targetEntityId } is in flight`)
683
+ metrics.push([ 'agency.communicate.mid_turn', 1 ])
684
+ return false
685
+ }
686
+
519
687
  // Content authored upstream when present (host / host-facet), else words a facet
520
688
  // authored off-tick and landed since a previous tick asked for them. Neither ⇒
521
689
  // request authoring (never awaited here — see `_authoring`) and hold 'awaiting'.
522
690
  const authored = str( intent.parameters['content'] ) ?? firstMessage( intent.parameters['messages'] )
691
+ const fromFacet = !authored
523
692
  let bubbles: string[] = authored ? [ authored ] : ( this._authored.get( id ) ?? [] )
524
693
  this._authored.delete( id )
525
694
  if( bubbles.length === 0 ){
526
- this._requestAuthoring( id, intent )
695
+ this._requestAuthoring( id, intent, tick )
527
696
  return false // nothing to send yet → await
528
697
  }
529
698
 
699
+ // ── Is this still what I want to say? ─────────────────────────
700
+ // Words a facet composed off-tick were formed against the situation as it
701
+ // stood when they were asked for. Delivering them into a situation that has
702
+ // since moved is not the act that was decided on — it is an older act
703
+ // arriving late wearing the present's clothes.
704
+ //
705
+ // Live: a COO committed to outreach, was told "I need my full attention on
706
+ // that right now, will brief you later", acknowledged that correctly — and
707
+ // 41 seconds later delivered the three-bubble message composed beforehand,
708
+ // asking for a brain-dump. Same shape at 3am with a colleague who had just
709
+ // said goodnight: "Night. Talk soon." followed one second later by two
710
+ // unprompted messages. Satiation cannot damp either, because satiation gates
711
+ // SELECTION and these were already selected. Nothing looked at the door.
712
+ //
713
+ // Only facet-composed words are checked. Content supplied upstream
714
+ // (`parameters.content`) came from the host or a host-facet with the present
715
+ // situation in hand, and a reply never reaches this path at all — it is
716
+ // delivered by the audition facet through the outbox.
717
+ const moved = fromFacet
718
+ ? situationMoved( state, intent.targetEntityId, this._composedAt.get( id ) )
719
+ : null
720
+ if( moved ){
721
+ this._withheld.set( id, moved )
722
+ this._composedAt.delete( id )
723
+ logger.debug(`[motor] "${ intent.schema }" withheld — ${ moved }`)
724
+ metrics.push([ 'agency.communicate.stale', 1 ])
725
+ return false // resolved as withheld by the sweep: freed, and nothing taught
726
+ }
727
+ this._composedAt.delete( id )
728
+
530
729
  const request: ActionRequest = {
531
730
  effector,
532
731
  parameters: { ...intent.parameters, messages: bubbles },
@@ -567,7 +766,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
567
766
 
568
767
  this._emitEnacted( intent, out, predicted, tick )
569
768
  this._emitActionOutcome( intent, result.success, result.feedback.outcomeQuality,
570
- clamp01( Math.abs( predicted.expectedReward - result.feedback.outcomeQuality ) ), tick )
769
+ clamp01( Math.abs( predicted.expectedReward - result.feedback.outcomeQuality ) ), tick,
770
+ out.description )
571
771
  metrics.push([ 'agency.communicate.delivered', 1 ])
572
772
  return true
573
773
  }
@@ -578,16 +778,46 @@ export class MotorSchemaExecutor implements CognitiveEngine {
578
778
  * per intent — a request already in flight is not duplicated, so the intent may sit
579
779
  * 'awaiting' across many ticks with exactly one LLM call behind it.
580
780
  */
581
- private _requestAuthoring( id: string, intent: Intent ): void {
781
+ private _requestAuthoring( id: string, intent: Intent, tick: Tick ): void {
582
782
  if( !this._author || this._authoring.has( id ) ) return
583
783
 
584
784
  const name = str( intent.parameters['targetEntityName'] ) ?? intent.targetEntityId ?? 'them'
585
785
  this._authoring.add( id )
786
+ this._composedAt.set( id, tick )
586
787
  void this._author
587
788
  .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 }"`)
789
+ .then( result => {
790
+ const bubbles = Array.isArray( result ) ? result : result.bubbles
791
+ const declared = !Array.isArray( result ) && result.withheld === true
792
+ const answered = !Array.isArray( result ) && result.answered === true
793
+
794
+ if( bubbles.length > 0 ){ this._authored.set( id, bubbles ); return }
795
+
796
+ // Not a warning: the facet was asked and answered. Choosing not to speak
797
+ // is a decision the mind is entitled to make.
798
+ if( declared ){
799
+ this._withheld.set( id, 'I considered speaking and chose not to.')
800
+ logger.debug(`[motor] "${ intent.schema }" withheld — the facet chose silence`)
801
+ return
802
+ }
803
+ // Asked, reasoned, and came back with nothing — neither words nor a
804
+ // declared silence. That is still an answer, and re-asking puts the same
805
+ // question to the same mind against the same situation, so it comes back
806
+ // the same way. Nineteen times, in the run this exists to prevent.
807
+ //
808
+ // Resolved as withheld rather than failed on purpose: nothing was said,
809
+ // and nothing should be taught about being bad at speaking — the #123
810
+ // regression. The reason is worded for what actually happened.
811
+ if( answered ){
812
+ this._withheld.set( id, 'I turned it over and came back with no words.')
813
+ logger.info(`[motor] "${ intent.schema }" withheld — the pass produced no words`)
814
+ return
815
+ }
816
+
817
+ // No pass ran at all — a dead author, a full budget, or one deferring to
818
+ // a pass already in flight. Keep holding: the clock is the right judge of
819
+ // those, and the deferring case wants to come back round.
820
+ logger.warn(`[motor] outreach authoring never ran for "${ intent.schema }"`)
591
821
  } )
592
822
  .catch( err => { logger.warn(`[motor] outreach authoring failed: ${ errMsg( err ) }`) } )
593
823
  .finally( () => { this._authoring.delete( id ) } )
@@ -649,6 +879,14 @@ export class MotorSchemaExecutor implements CognitiveEngine {
649
879
 
650
880
  private _emitActionOutcome(
651
881
  intent: Intent, success: boolean, outcomeQuality: number, surprise: number, tick: Tick,
882
+ /**
883
+ * What happened, in words. This payload builds `action.record`, which the
884
+ * prompt renders as `## What Became Of What I Did` — and until now this method
885
+ * published no description at all, so that section showed the action's NAME
886
+ * and nothing else. Sixty-five lookups rendered as sixty-five lines saying
887
+ * `discord_lookup_member` and never once what was found.
888
+ */
889
+ description?: string,
652
890
  ): void {
653
891
  if( !this._bus ) return
654
892
  try {
@@ -659,6 +897,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
659
897
  actionType: intent.schema,
660
898
  domain: intent.schema,
661
899
  confidence: intent.expectedReward,
900
+ ...( description ? { description } : {} ),
662
901
  success,
663
902
  outcomeQuality,
664
903
  surprise,
@@ -672,21 +911,67 @@ export class MotorSchemaExecutor implements CognitiveEngine {
672
911
  catch( err ){ logger.warn(`[motor] action outcome publish failed: ${ errMsg( err ) }`) }
673
912
  }
674
913
 
675
- private _emitDispatch( intent: Intent, mode: 'communicate' | 'external', tick: Tick ): void {
676
- if( !this._bus ) return
677
- try {
678
- this._bus.publish({
679
- type: mode === 'communicate' ? 'agency.communicate' : 'agency.invocation',
680
- version: 1, sourceEngine: this.name, salience: 0.6,
681
- payload: {
914
+ /**
915
+ * Announce a dispatched act to its TWO audiences, which live on two buses.
916
+ *
917
+ * The mind's own faculties appraise it — affective.blender, stress.regulator
918
+ * and attention.allocator all subscribe to `agency.invocation` on the
919
+ * CognitiveBus, the internal fabric. But the act is host-owned, and the HOST
920
+ * hears only `simulation.eventBus`: `WillStem` buffers an invocation for
921
+ * delivery from `eventBus.subscribeAll`, and that is the sole path by which an
922
+ * external effector ever reaches a handler.
923
+ *
924
+ * Only the cognitive half was ever published. The two buses have no bridge, so
925
+ * `bufferInvocation` was subscribed to a bus that has never carried the event
926
+ * — measured on a live boot: `agency.invocation.dispatched` incremented,
927
+ * `pendingEffectorInvocations` stayed empty, the intent held `awaiting`, and
928
+ * fifteen ticks later `[motor] ⏱ "inspect" timed out`. **No host-owned
929
+ * effector invocation has ever been delivered.** Communication is unaffected —
930
+ * the outbox is a separate mechanism, which is why a Will could always speak.
931
+ *
932
+ * Both ends were unit-tested and the crossing was not: `policy.*.test.ts` calls
933
+ * `bufferInvocation` directly, which is true about the controller and silent
934
+ * about whether anything reaches it. Same shape as the affordance-field hop.
935
+ */
936
+ private _emitDispatch(
937
+ intent: Intent, mode: 'communicate' | 'external', tick: Tick,
938
+ state?: ReadonlySimulationState,
939
+ ): Array<Omit<SimulationEvent, 'id' | 'timestamp' | 'tick'>> {
940
+ const payload = this._dispatchPayload( intent, tick, state )
941
+ const type = mode === 'communicate' ? 'agency.communicate' : 'agency.invocation'
942
+
943
+ if( this._bus )
944
+ try {
945
+ this._bus.publish({ type, version: 1, sourceEngine: this.name, salience: 0.6, payload })
946
+ }
947
+ catch( err ){ logger.warn(`[motor] dispatch publish failed: ${ errMsg( err ) }`) }
948
+
949
+ // The observable bus — where the host is listening.
950
+ return [ { type, source: this.name, payload } ]
951
+ }
952
+
953
+ private _dispatchPayload(
954
+ intent: Intent, tick: Tick, state?: ReadonlySimulationState,
955
+ ): Record<string, unknown> {
956
+ return {
682
957
  schema: intent.schema, intentId: intent.id,
683
958
  targetEntityId: intent.targetEntityId, parameters: intent.parameters, tick,
684
959
  // The ability's declared meaning, carried to the host handler.
685
960
  description: this._resolve( intent.schema )?.description,
686
- },
687
- })
961
+ // Where in the world this referent is, so a host can act on it at all.
962
+ //
963
+ // The target is an ANCHOR (`ke:1sqlkux`) — 0.9.0 made identity opaque and
964
+ // separate from address on purpose. A bridge holds channel ids and knows
965
+ // nothing of anchors, so an invocation naming only the referent is one no
966
+ // surface can serve. The outbox hit this first and solved it the same way
967
+ // (mind.ts attachRouting): resolve inside, where the alias table lives.
968
+ //
969
+ // Outbound only. This is the mind telling the world which of its own
970
+ // handles it means — the reverse direction, a surface writing an address
971
+ // into the mind, is what perception is for.
972
+ ...( state && intent.targetEntityId
973
+ ? { targetAddresses: addressesOf( state.entities, intent.targetEntityId ) } : {} ),
688
974
  }
689
- catch( err ){ logger.warn(`[motor] dispatch publish failed: ${ errMsg( err ) }`) }
690
975
  }
691
976
  }
692
977
 
@@ -750,3 +1035,53 @@ function clamp01( n: number ): number {
750
1035
  function errMsg( err: unknown ): string {
751
1036
  return err instanceof Error ? err.message : String( err )
752
1037
  }
1038
+
1039
+ /**
1040
+ * Has the situation these words were composed for moved on?
1041
+ *
1042
+ * Returns the mind's own account of what changed, or null if the moment still
1043
+ * stands. The account is what the withheld outcome records, so the mind's
1044
+ * history says why it did not speak rather than merely that it didn't.
1045
+ *
1046
+ * The evidence is `conversation.sent` — the durable record of having spoken,
1047
+ * written by every path that speaks (ProactiveCommunicator, the audition facet,
1048
+ * the outbox). If I have said something to this person SINCE these words were
1049
+ * asked for, then these are not my current words to them: I have already
1050
+ * responded to whatever moved in between, and this is an older turn arriving on
1051
+ * top of a newer one.
1052
+ *
1053
+ * And `answeredAt` — when they last spoke to ME. This arm was missing at first,
1054
+ * excluded on the argument that `conversation.received` is a one-shot entity
1055
+ * swept on the tick it is scanned so nothing durable records the other
1056
+ * direction. That was wrong: `answeredAt` is the durable half, folded onto the
1057
+ * mind's own sent turns, and it is what renders "they answered" in the prompt.
1058
+ *
1059
+ * The gap was the COMMON case, not an edge. Live, a COO delivered a pre-composed
1060
+ * agenda message ("I need a list of what's actively in flight…") two seconds
1061
+ * after the person changed the subject, having said nothing in between — so the
1062
+ * stale words arrived BEFORE her real reply and the spoken-since arm had not
1063
+ * fired yet. The two signals were assumed to coincide and do not: whoever moved
1064
+ * the situation, it moved.
1065
+ *
1066
+ * A missing `composedAt` means the words were never requested through
1067
+ * `_requestAuthoring` — nothing is known about when they were formed, so nothing
1068
+ * is claimed and they go out. Silence beats a guess here: withholding on absent
1069
+ * evidence would mute a mind for reasons it could not name.
1070
+ */
1071
+ export function situationMoved(
1072
+ state: ReadonlySimulationState,
1073
+ targetEntityId: string | undefined,
1074
+ composedAt: Tick | undefined,
1075
+ ): string | null {
1076
+ if( composedAt === undefined || !targetEntityId ) return null
1077
+
1078
+ const heard = lastAnsweredByEntity( state.entities as never ).get( targetEntityId )
1079
+ if( heard !== undefined && heard > composedAt )
1080
+ return 'They said something after I composed this, so it is an answer to a moment that has passed.'
1081
+
1082
+ const spoke = spokenAtByEntity( state.entities as never ).get( targetEntityId )
1083
+ if( spoke !== undefined && spoke > composedAt )
1084
+ return 'I had already spoken to them since composing this, so it was no longer what I had to say.'
1085
+
1086
+ return null
1087
+ }