@mindot/will 0.8.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 (142) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +6985 -3337
  4. package/dist/index.js.map +1 -1
  5. package/dist/surface/channels/discord.d.ts +177 -0
  6. package/dist/surface/channels/discord.js +394 -0
  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 +11 -5
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +5382 -1573
  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-cS6k4uiJ.d.ts → will-evj9_vrd.d.ts} +6658 -5078
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +356 -1
  19. package/src/cognition/agency/conversation.aim.ts +292 -0
  20. package/src/cognition/agency/engines/action.selector.ts +142 -5
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +200 -7
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +509 -32
  24. package/src/cognition/agency/engines/reafference.engine.ts +160 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +5 -4
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/innate.ts +96 -1
  30. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  31. package/src/cognition/agency/selection.scoring.ts +49 -0
  32. package/src/cognition/agency/settlement.ts +203 -0
  33. package/src/cognition/agency/types.ts +73 -1
  34. package/src/cognition/cache/composition.ts +232 -0
  35. package/src/cognition/cache/deliberation.cache.ts +219 -0
  36. package/src/cognition/cache/fingerprint.ts +120 -0
  37. package/src/cognition/cache/types.ts +105 -0
  38. package/src/cognition/config.mirror.entities.ts +108 -1
  39. package/src/cognition/event.schemas.ts +22 -8
  40. package/src/cognition/faculties/affective.blender.ts +21 -4
  41. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  42. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  43. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  44. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  45. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  46. package/src/cognition/faculties/executive.engine/context.ts +106 -46
  47. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  48. package/src/cognition/faculties/executive.engine/engine.ts +714 -136
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +230 -45
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +320 -65
  51. package/src/cognition/faculties/executive.engine/facet.ts +81 -26
  52. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  53. package/src/cognition/faculties/executive.engine/parser.ts +120 -38
  54. package/src/cognition/faculties/executive.engine/prompt.factory.ts +331 -38
  55. package/src/cognition/faculties/executive.engine/types.ts +84 -12
  56. package/src/cognition/faculties/exteroception.ts +99 -98
  57. package/src/cognition/faculties/goal.manager.ts +144 -16
  58. package/src/cognition/faculties/known.entity.tracker.ts +287 -29
  59. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  60. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  61. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  62. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  63. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  64. package/src/cognition/faculties/self.model.updater.ts +19 -12
  65. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  66. package/src/cognition/faculties/social.perception.ts +47 -3
  67. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  68. package/src/cognition/faculties/working.memory.ts +16 -21
  69. package/src/cognition/identity.entity.ts +205 -0
  70. package/src/cognition/index.ts +7 -0
  71. package/src/cognition/instruction.handler.ts +1 -1
  72. package/src/cognition/memory/vector.adapter.ts +12 -3
  73. package/src/cognition/memory/vector.embedder.ts +45 -4
  74. package/src/cognition/percept.entity.ts +122 -0
  75. package/src/cognition/persona.prior.ts +6 -0
  76. package/src/cognition/sense.boundary.ts +176 -0
  77. package/src/cognition/senses/audition.engine/engine.ts +492 -53
  78. package/src/cognition/senses/base.sense.engine.ts +105 -7
  79. package/src/cognition/senses/index.ts +83 -13
  80. package/src/cognition/senses/provenance.ts +128 -0
  81. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  82. package/src/cognition/social.identity.ts +296 -0
  83. package/src/cognition/utilities/token.tracker.ts +94 -10
  84. package/src/core/orchestrator.ts +51 -0
  85. package/src/index.ts +39 -5
  86. package/src/llm/gate.ts +48 -0
  87. package/src/llm/index.ts +127 -33
  88. package/src/llm/routing.ts +6 -0
  89. package/src/llm/summarizer.ts +1 -1
  90. package/src/llm/wire.contracts.ts +57 -0
  91. package/src/pma/index.ts +68 -54
  92. package/src/stem/assembly.audit.ts +1 -0
  93. package/src/stem/guards/identity.coherence.ts +1 -1
  94. package/src/stem/index.ts +171 -23
  95. package/src/stem/mind.ts +198 -57
  96. package/src/stem/policy/arbiter.ts +10 -0
  97. package/src/stem/profiles/built-in.ts +7 -0
  98. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  99. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  100. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  101. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  102. package/src/{profiles → stem/profiles}/index.ts +1 -1
  103. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  104. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  105. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  106. package/src/stem/tracts/effector/types.ts +110 -0
  107. package/src/stem/tracts/effector.controller.ts +112 -401
  108. package/src/stem/tracts/outbox.controller.ts +30 -17
  109. package/src/stem/tracts/outbox.writer.ts +40 -2
  110. package/src/stem/tracts/sensory.controller.ts +6 -6
  111. package/src/stem/tracts/session.logger.ts +6 -1
  112. package/src/stem/tracts/transport/types.ts +20 -1
  113. package/src/stem/tracts/transport.controller.ts +33 -9
  114. package/src/surface/channels/discord.ts +609 -0
  115. package/src/{channels → surface/channels}/roster.ts +1 -1
  116. package/src/surface/channels/types.ts +136 -0
  117. package/src/{channels → surface/channels}/whatsapp.ts +22 -10
  118. package/src/{cli.ts → surface/cli.ts} +14 -9
  119. package/src/{host → surface/host}/boot.ts +3 -3
  120. package/src/{host → surface/host}/utterances.ts +2 -2
  121. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  122. package/src/{mcp → surface/mcp}/server.ts +19 -5
  123. package/src/{sdk → surface/sdk}/will.ts +194 -33
  124. package/src/{serve → surface/serve}/server.ts +11 -4
  125. package/src/types.ts +24 -3
  126. package/dist/channels/discord.d.ts +0 -69
  127. package/dist/channels/discord.js +0 -193
  128. package/dist/channels/discord.js.map +0 -1
  129. package/dist/channels/whatsapp.js.map +0 -1
  130. package/dist/cli.js.map +0 -1
  131. package/dist/mcp/effectors.js.map +0 -1
  132. package/src/channels/discord.ts +0 -214
  133. package/src/channels/types.ts +0 -46
  134. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
  135. package/src/extensions/livestream.ext.ts +0 -570
  136. package/src/extensions/time.ext.ts +0 -339
  137. package/src/profiles/built-in.ts +0 -7
  138. package/src/runners/coherence.runner.ts +0 -49
  139. package/src/runners/outreach.runner.ts +0 -155
  140. package/src/runners/social.runner.ts +0 -179
  141. package/src/runners/thin-shim.runner.ts +0 -205
  142. /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)
@@ -71,11 +82,48 @@ interface Intent {
71
82
  */
72
83
  planId?: string
73
84
  planStepId?: string
85
+ /**
86
+ * The entity that evoked this — for an ideomotor winner, the `ideomotor.intent`
87
+ * the executive wrote. Read so enaction can retire it (see _dischargeWill).
88
+ */
89
+ evokedBy?: string
90
+ }
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
74
116
  }
75
117
 
76
118
  /** Authors the words for a self-initiated communicate the agency selected (no inbound triggered it). */
77
119
  export interface OutreachAuthor {
78
- 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
79
127
  }
80
128
 
81
129
  export class MotorSchemaExecutor implements CognitiveEngine {
@@ -88,6 +136,46 @@ export class MotorSchemaExecutor implements CognitiveEngine {
88
136
  private _grants: AccessGrants | null = null
89
137
  private _bus: CognitiveBus | null = null
90
138
 
139
+ /**
140
+ * Two-phase outreach authoring. A facet cannot be awaited from inside a tick:
141
+ * `ExecutiveFacet.report()` only QUEUES in tick-discipline mode and the reasoning
142
+ * launches from `pump()`, which the ExecutiveEngine calls once per tick — so an
143
+ * in-tick `await` blocks the very loop that would produce the answer. Observed
144
+ * live: a 61s freeze of the whole mind inside one tick, then an empty result.
145
+ * (It passes unit tests because bare facets have no inbox and author inline.)
146
+ *
147
+ * So `_deliver` REQUESTS words and returns false (the intent holds 'awaiting'),
148
+ * the facet answers off-tick, and a later tick delivers. Process-local by design:
149
+ * an authoring call in flight cannot survive a restart, and the intent would
150
+ * simply re-request.
151
+ */
152
+ private _authoring = new Set<string>()
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>()
178
+
91
179
  constructor( schemas: MotorSchema[] = INNATE_SCHEMAS ){
92
180
  for( const s of schemas ) this._schemas.set( s.id, s )
93
181
  }
@@ -115,6 +203,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
115
203
  { type: 'agency.enacted', version: 1, validate: () => null },
116
204
  { type: 'agency.invocation', version: 1, validate: () => null },
117
205
  { type: 'agency.communicate', version: 1, validate: () => null },
206
+ { type: 'action.withheld', version: 1, validate: () => null },
118
207
  ]
119
208
  }
120
209
  subscribes(): string[] { return [] }
@@ -123,7 +212,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
123
212
  * The executor is plan-agnostic. A plan does NOT dispatch steps here — it biases
124
213
  * the affordance competition (see PLANNING_AS_PRIOR_TODO.md), so a plan-driven
125
214
  * action reaches the executor as an ordinary committed `agency.intent` the
126
- * selector won. That intent already carries planId/stepId provenance (stamped by
215
+ * selector won. That intent already carries its planId/stepId link (stamped by
127
216
  * the selector from the winning affordance); `_emitActionOutcome` threads it back
128
217
  * out, which is how the PlanningEngine advances. Nothing plan-specific here.
129
218
  */
@@ -140,6 +229,10 @@ export class MotorSchemaExecutor implements CognitiveEngine {
140
229
  const set: EntityInput[] = []
141
230
  const del: string[] = []
142
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'>> = []
143
236
 
144
237
  const energy = state.metrics.get('energy.level') ?? 100
145
238
  const stress = state.metrics.get('stress.load' ) ?? 0
@@ -152,6 +245,22 @@ export class MotorSchemaExecutor implements CognitiveEngine {
152
245
  if( tick >= num( e.metadata?.['expiresAt'], 0 ) ) del.push( id )
153
246
  }
154
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
+
155
264
  // ── Revocation tombstones (EXAFFERENCE P4) ───────────────────
156
265
  // Reap expired tombstones (intent vanished otherwise), and collect live ones
157
266
  // so the selected-processing loop can refuse the half-race case: Deliberation
@@ -173,18 +282,122 @@ export class MotorSchemaExecutor implements CognitiveEngine {
173
282
  else if( parentId && revoked.has( parentId ) ) del.push( id )
174
283
  }
175
284
 
285
+ // ── Words that landed off-tick → deliver now ─────────────────
286
+ // Second half of two-phase authoring: a previous tick asked a facet for the words
287
+ // and held the intent 'awaiting'; the facet answered between ticks. Runs BEFORE
288
+ // the timeout sweep so words arriving on the same tick the clock would expire are
289
+ // still spoken rather than discarded.
290
+ const spokeThisTick = new Set<string>()
291
+ for( const [ id, e ] of state.entities ){
292
+ if( e.type !== 'agency.intent' || str( e.metadata?.['status'] ) !== 'awaiting') continue
293
+ if( !this._authored.has( id ) ) continue
294
+
295
+ const intent = readIntent( id, e.metadata )
296
+ const predicted: EfferenceCopy = {
297
+ expectedReward: num( e.metadata?.['predictedReward'], intent.expectedReward ),
298
+ expectedValence: num( e.metadata?.['predictedValence'], intent.expectedValence ),
299
+ }
300
+ if( await this._deliver( id, intent, predicted, state, tick, set, del, metrics ) )
301
+ spokeThisTick.add( id )
302
+ }
303
+
304
+ // Words whose intent no longer exists (revoked, timed out, superseded) are words
305
+ // that will never be said — drop them so the map cannot grow without bound.
306
+ for( const id of this._authored.keys() )
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 )
312
+
176
313
  // ── Timeout stranded async intents ───────────────────────────
177
314
  // An 'awaiting' intent whose host/delivery never returned would block the
178
315
  // serial Will forever. After AWAIT_TIMEOUT ticks, abandon it as a failed
179
316
  // outcome (which also teaches reafference the action is unreliable here).
180
317
  for( const [ id, e ] of state.entities ){
181
318
  if( e.type !== 'agency.intent' || str( e.metadata?.['status'] ) !== 'awaiting') continue
319
+ // Already delivered above — its outcome and deletion are queued for this tick.
320
+ if( spokeThisTick.has( id ) ) continue
321
+ // Words are being authored right now. The clock PAUSES rather than extends: a
322
+ // facet LLM call is 10–30s of latency we do not control, and any fixed budget
323
+ // large enough to cover a slow one is also large enough to strand a dead one.
324
+ // While a call is genuinely in flight the world has not failed to answer. The
325
+ // pause is bounded, not open-ended: `authorOutreach` always settles (its own 60s
326
+ // timeout resolves empty), so `_authoring` always clears and the clock resumes.
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
+ }
182
370
  // POLICY_REAFFERENCE P4 — an escalated intent is HELD: the stem owns its
183
371
  // lifecycle (extended TTL → approve/deny/expire), so the executor must not
184
372
  // time it out at AWAIT_TIMEOUT and reconcile it as a phantom failure.
185
373
  if( e.metadata?.['escalated'] === true ) continue
186
374
  const dispatchedAt = num( e.metadata?.['dispatchedAt'], tick )
187
- if( tick - dispatchedAt < AWAIT_TIMEOUT ) continue
375
+ const age = tick - dispatchedAt
376
+
377
+ // Dispatched LATER than now ⇒ it was in flight when the mind went to sleep.
378
+ // Intents snapshot with the state and the tick counter restarts at 1 on wake,
379
+ // so a restored `awaiting` intent has a NEGATIVE age — and every guard here
380
+ // inverts on it: `age < AWAIT_TIMEOUT` is trivially true for -589, so it never
381
+ // timed out, and the selector's staleness went negative, INFLATING the
382
+ // incumbent's strength instead of decaying it. Measured on a live Will:
383
+ // incumbent 9.742 against challengers of 0.52, unpreemptable and immortal.
384
+ // One person's stale intent held the channel and every attempt to contact
385
+ // anyone else was refused, indefinitely, across restarts.
386
+ //
387
+ // Cleared WITHOUT a failure outcome: hibernating is not the world declining
388
+ // to answer. Reconciling it as a timeout would teach reafference that
389
+ // reaching that person does not work, which is a lesson about the process
390
+ // lifecycle, not about them.
391
+ if( age < 0 ){
392
+ del.push( id )
393
+ logger.info(
394
+ `[motor] cleared "${ str( e.metadata?.['schema'] ) ?? 'intent' }" left awaiting ` +
395
+ `across a restart (dispatched at tick ${ dispatchedAt }, now ${ tick })`
396
+ )
397
+ continue
398
+ }
399
+
400
+ if( age < AWAIT_TIMEOUT ) continue
188
401
 
189
402
  const intent = readIntent( id, e.metadata )
190
403
  const predicted: EfferenceCopy = {
@@ -199,7 +412,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
199
412
  del.push( id )
200
413
  this._emitEnacted( intent, timedOut, predicted, tick )
201
414
  if( intent.planId && intent.planStepId )
202
- 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.` )
203
417
  logger.info(`[motor] ⏱ "${ intent.schema }" timed out after ${ tick - dispatchedAt } ticks`)
204
418
  }
205
419
 
@@ -242,7 +456,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
242
456
  enactedCount++
243
457
  this._emitEnacted( intent, enaction, predicted, tick )
244
458
  this._emitActionOutcome( intent, enaction.success, enaction.outcomeQuality,
245
- clamp01( Math.abs( predicted.expectedReward - enaction.outcomeQuality ) ), tick )
459
+ clamp01( Math.abs( predicted.expectedReward - enaction.outcomeQuality ) ), tick,
460
+ enaction.description )
246
461
 
247
462
  // If this was a macro step, advance (or finalize) its parent.
248
463
  if( intent.parentIntentId )
@@ -281,6 +496,13 @@ export class MotorSchemaExecutor implements CognitiveEngine {
281
496
  set.push( consequenceEntity({
282
497
  intentId: id, schema: intent.schema,
283
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 } : {} ),
284
506
  ...( enaction.mode === 'communicate'
285
507
  ? { effector: COMM_SCHEMA_TO_EFFECTOR[ intent.schema ] ?? intent.schema }
286
508
  : {} ),
@@ -289,7 +511,16 @@ export class MotorSchemaExecutor implements CognitiveEngine {
289
511
  paramsHash: fnv1a( paramsKey( intent.parameters ) ),
290
512
  expiresAt: tick + CONSEQUENCE_TTL_TICKS, tick,
291
513
  }) )
292
- 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 ) )
293
524
  metrics.push([ enaction.mode === 'communicate'
294
525
  ? 'agency.communicate.dispatched'
295
526
  : 'agency.invocation.dispatched', 1 ])
@@ -298,7 +529,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
298
529
  }
299
530
 
300
531
  metrics.push([ 'agency.executor.enacted', enactedCount ])
301
- return { commands: { set, delete: del, metrics } }
532
+ return { commands: { set, delete: del, metrics }, ...( events.length > 0 ? { events } : {} ) }
302
533
  }
303
534
 
304
535
  // ── composite machinery ──────────────────────────────────────
@@ -374,7 +605,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
374
605
  this._emitEnacted( compIntent, compEnaction,
375
606
  { expectedReward: compIntent.expectedReward, expectedValence: compIntent.expectedValence }, tick )
376
607
  this._emitActionOutcome( compIntent, true, avgQuality,
377
- clamp01( Math.abs( compIntent.expectedReward - avgQuality ) ), tick )
608
+ clamp01( Math.abs( compIntent.expectedReward - avgQuality ) ), tick,
609
+ compEnaction.description )
378
610
  }
379
611
 
380
612
  private _subIntent(
@@ -437,17 +669,62 @@ export class MotorSchemaExecutor implements CognitiveEngine {
437
669
  return true
438
670
  }
439
671
 
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.
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
+
687
+ // Content authored upstream when present (host / host-facet), else words a facet
688
+ // authored off-tick and landed since a previous tick asked for them. Neither ⇒
689
+ // request authoring (never awaited here — see `_authoring`) and hold 'awaiting'.
443
690
  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 ) }`) }
691
+ const fromFacet = !authored
692
+ let bubbles: string[] = authored ? [ authored ] : ( this._authored.get( id ) ?? [] )
693
+ this._authored.delete( id )
694
+ if( bubbles.length === 0 ){
695
+ this._requestAuthoring( id, intent, tick )
696
+ return false // nothing to send yet → await
697
+ }
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
449
726
  }
450
- if( bubbles.length === 0 ) return false // nothing authored to send → await
727
+ this._composedAt.delete( id )
451
728
 
452
729
  const request: ActionRequest = {
453
730
  effector,
@@ -477,13 +754,75 @@ export class MotorSchemaExecutor implements CognitiveEngine {
477
754
  text: bubbles.join('\n'),
478
755
  expiresAt: tick + CONSEQUENCE_TTL_TICKS, tick,
479
756
  }) )
757
+ // The words are out. Whatever willed them is DONE being an intention.
758
+ //
759
+ // Discharged here rather than at dispatch on purpose: `_deliver` holds the
760
+ // intent 'awaiting' while a facet authors, and that authoring can time out or
761
+ // return nothing. Clearing the will at dispatch would lose the intention
762
+ // silently — the mind would have decided to say something and simply never
763
+ // have, which is the failure `commands.ts` calls "the whole intention
764
+ // evaporated without a trace". Enaction is the discharge; a request is not.
765
+ if( result.success ) this._dischargeWill( intent, del )
766
+
480
767
  this._emitEnacted( intent, out, predicted, tick )
481
768
  this._emitActionOutcome( intent, result.success, result.feedback.outcomeQuality,
482
- clamp01( Math.abs( predicted.expectedReward - result.feedback.outcomeQuality ) ), tick )
769
+ clamp01( Math.abs( predicted.expectedReward - result.feedback.outcomeQuality ) ), tick,
770
+ out.description )
483
771
  metrics.push([ 'agency.communicate.delivered', 1 ])
484
772
  return true
485
773
  }
486
774
 
775
+ /**
776
+ * Ask a facet for the words, off-tick. Fire-and-forget on purpose: awaiting this
777
+ * from inside `react()` deadlocks the tick loop against the facet pump. Idempotent
778
+ * per intent — a request already in flight is not duplicated, so the intent may sit
779
+ * 'awaiting' across many ticks with exactly one LLM call behind it.
780
+ */
781
+ private _requestAuthoring( id: string, intent: Intent, tick: Tick ): void {
782
+ if( !this._author || this._authoring.has( id ) ) return
783
+
784
+ const name = str( intent.parameters['targetEntityName'] ) ?? intent.targetEntityId ?? 'them'
785
+ this._authoring.add( id )
786
+ this._composedAt.set( id, tick )
787
+ void this._author
788
+ .authorOutreach( intent.targetEntityId ?? '', name, str( intent.parameters['gist'] ) )
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 }"`)
821
+ } )
822
+ .catch( err => { logger.warn(`[motor] outreach authoring failed: ${ errMsg( err ) }`) } )
823
+ .finally( () => { this._authoring.delete( id ) } )
824
+ }
825
+
487
826
  // ── bus emission ─────────────────────────────────────────────
488
827
 
489
828
  private _emitEnacted( intent: Intent, enaction: Enaction, predicted: EfferenceCopy, tick: Tick ): void {
@@ -506,8 +845,48 @@ export class MotorSchemaExecutor implements CognitiveEngine {
506
845
  * RewardEvaluator reads it as a reward signal. `confidence` carries the agency's
507
846
  * own forward-model prior so calibration has a real prediction to score.
508
847
  */
848
+ /**
849
+ * Retire the `ideomotor.intent` that produced this act.
850
+ *
851
+ * Nothing deleted these. They were cleared only when the executive next ran and
852
+ * declined to name the same action again — and the executive runs on an interval,
853
+ * so between cycles a willed reach-out STOOD in state, was rebuilt into an
854
+ * affordance every single tick, and competed every single tick. Observed as
855
+ * dozens of identical lines:
856
+ *
857
+ * [selector] willed reach-out → … NOT selected: 0.297 < inspect… 0.340
858
+ *
859
+ * losing by four thousandths, over and over, until it won — twice. Fabrice got
860
+ * the same message byte-for-byte 25 ticks apart, two outbox ids.
861
+ *
862
+ * `justEnacted` was built to hold this line and cannot: it is a DECAYING
863
+ * quantity, capped at `repeatDamping` (0.30), and a standing intent outlasts it
864
+ * by construction. Damping a permanent pull only ever delays it. So the intention
865
+ * is discharged by being acted on, which is what an intention is — you meant to
866
+ * tell someone something, you told them, and it is finished. If the mind still
867
+ * wants to say more, the next executive cycle forms a new one, now seeing "I said
868
+ * this 25 ticks ago and have had no answer" in front of it.
869
+ *
870
+ * Satiation stays exactly as it was, and still earns its keep: it damps saying
871
+ * the same thing again for reasons that did NOT come from a standing will.
872
+ */
873
+ private _dischargeWill( intent: Intent, del: string[] ): void {
874
+ const willId = intent.evokedBy
875
+ if( !willId || !willId.startsWith('ideomotor-') ) return
876
+ del.push( willId )
877
+ logger.info(`[motor] discharged the will behind "${ intent.schema }" (${ willId })`)
878
+ }
879
+
509
880
  private _emitActionOutcome(
510
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,
511
890
  ): void {
512
891
  if( !this._bus ) return
513
892
  try {
@@ -518,6 +897,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
518
897
  actionType: intent.schema,
519
898
  domain: intent.schema,
520
899
  confidence: intent.expectedReward,
900
+ ...( description ? { description } : {} ),
521
901
  success,
522
902
  outcomeQuality,
523
903
  surprise,
@@ -531,21 +911,67 @@ export class MotorSchemaExecutor implements CognitiveEngine {
531
911
  catch( err ){ logger.warn(`[motor] action outcome publish failed: ${ errMsg( err ) }`) }
532
912
  }
533
913
 
534
- private _emitDispatch( intent: Intent, mode: 'communicate' | 'external', tick: Tick ): void {
535
- if( !this._bus ) return
536
- try {
537
- this._bus.publish({
538
- type: mode === 'communicate' ? 'agency.communicate' : 'agency.invocation',
539
- version: 1, sourceEngine: this.name, salience: 0.6,
540
- 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 {
541
957
  schema: intent.schema, intentId: intent.id,
542
958
  targetEntityId: intent.targetEntityId, parameters: intent.parameters, tick,
543
959
  // The ability's declared meaning, carried to the host handler.
544
960
  description: this._resolve( intent.schema )?.description,
545
- },
546
- })
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 ) } : {} ),
547
974
  }
548
- catch( err ){ logger.warn(`[motor] dispatch publish failed: ${ errMsg( err ) }`) }
549
975
  }
550
976
  }
551
977
 
@@ -588,6 +1014,7 @@ function readIntent( id: string, m: ReadonlyMap<string, unknown> | Record<string
588
1014
  stepIndex: typeof meta['stepIndex'] === 'number' ? ( meta['stepIndex'] as number ) : undefined,
589
1015
  planId: str( meta['planId'] ),
590
1016
  planStepId: str( meta['stepId'] ),
1017
+ evokedBy: str( meta['evokedBy'] ),
591
1018
  }
592
1019
  }
593
1020
 
@@ -608,3 +1035,53 @@ function clamp01( n: number ): number {
608
1035
  function errMsg( err: unknown ): string {
609
1036
  return err instanceof Error ? err.message : String( err )
610
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
+ }