@mindot/will 0.8.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 (79) hide show
  1. package/dist/channels/discord.d.ts +67 -6
  2. package/dist/channels/discord.js +112 -6
  3. package/dist/channels/discord.js.map +1 -1
  4. package/dist/channels/whatsapp.d.ts +1 -1
  5. package/dist/channels/whatsapp.js +4 -1
  6. package/dist/channels/whatsapp.js.map +1 -1
  7. package/dist/cli.js +3174 -867
  8. package/dist/cli.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +3236 -1042
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp/effectors.d.ts +1 -1
  13. package/dist/{will-cS6k4uiJ.d.ts → will-DbDj_TEH.d.ts} +752 -17
  14. package/package.json +1 -1
  15. package/src/channels/discord.ts +189 -11
  16. package/src/channels/types.ts +90 -0
  17. package/src/channels/whatsapp.ts +13 -4
  18. package/src/cli.ts +9 -4
  19. package/src/cognition/agency/consequence.ts +122 -1
  20. package/src/cognition/agency/conversation.aim.ts +260 -0
  21. package/src/cognition/agency/engines/action.selector.ts +83 -2
  22. package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
  24. package/src/cognition/agency/engines/reafference.engine.ts +117 -0
  25. package/src/cognition/agency/proactive.communicator.ts +19 -3
  26. package/src/cognition/agency/restart.ts +66 -0
  27. package/src/cognition/agency/selection.scoring.ts +33 -0
  28. package/src/cognition/agency/types.ts +35 -0
  29. package/src/cognition/cache/composition.ts +232 -0
  30. package/src/cognition/cache/deliberation.cache.ts +219 -0
  31. package/src/cognition/cache/fingerprint.ts +120 -0
  32. package/src/cognition/cache/types.ts +105 -0
  33. package/src/cognition/config.mirror.entities.ts +108 -0
  34. package/src/cognition/event.schemas.ts +22 -0
  35. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  36. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  37. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  38. package/src/cognition/faculties/executive.engine/context.ts +67 -13
  39. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  40. package/src/cognition/faculties/executive.engine/engine.ts +552 -131
  41. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
  42. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
  43. package/src/cognition/faculties/executive.engine/facet.ts +81 -26
  44. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  45. package/src/cognition/faculties/executive.engine/parser.ts +21 -1
  46. package/src/cognition/faculties/executive.engine/prompt.factory.ts +167 -19
  47. package/src/cognition/faculties/executive.engine/types.ts +69 -0
  48. package/src/cognition/faculties/goal.manager.ts +94 -14
  49. package/src/cognition/faculties/known.entity.tracker.ts +267 -28
  50. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  51. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  52. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  53. package/src/cognition/faculties/self.model.updater.ts +19 -12
  54. package/src/cognition/faculties/social.perception.ts +47 -3
  55. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  56. package/src/cognition/faculties/working.memory.ts +10 -20
  57. package/src/cognition/identity.entity.ts +205 -0
  58. package/src/cognition/index.ts +7 -0
  59. package/src/cognition/memory/vector.adapter.ts +12 -3
  60. package/src/cognition/memory/vector.embedder.ts +45 -2
  61. package/src/cognition/persona.prior.ts +6 -0
  62. package/src/cognition/senses/audition.engine/engine.ts +404 -46
  63. package/src/cognition/senses/base.sense.engine.ts +1 -1
  64. package/src/cognition/senses/index.ts +12 -0
  65. package/src/cognition/social.identity.ts +273 -0
  66. package/src/cognition/utilities/token.tracker.ts +58 -5
  67. package/src/core/orchestrator.ts +38 -0
  68. package/src/llm/index.ts +25 -8
  69. package/src/llm/routing.ts +6 -0
  70. package/src/llm/summarizer.ts +1 -1
  71. package/src/llm/wire.contracts.ts +19 -0
  72. package/src/pma/index.ts +67 -53
  73. package/src/sdk/will.ts +39 -6
  74. package/src/stem/assembly.audit.ts +1 -0
  75. package/src/stem/guards/identity.coherence.ts +1 -1
  76. package/src/stem/index.ts +79 -2
  77. package/src/stem/mind.ts +172 -55
  78. package/src/stem/tracts/outbox.writer.ts +40 -2
  79. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
@@ -13,6 +13,7 @@
13
13
  // reason about its own operational parameters.
14
14
 
15
15
  import { WillConfig } from '#stem/mind'
16
+ import type { StateManager } from '#core/state.manager'
16
17
 
17
18
  export interface EngineConfigEntity {
18
19
  id: string
@@ -316,6 +317,24 @@ export function buildEngineConfigEntities( config: WillConfig, executiveInterval
316
317
  // DOWN from demonstrated `analytical` disposition via the persona-prior mirror,
317
318
  // so a more analytical Will deliberates more readily; this is the baseline.
318
319
  deliberateThreshold: 0.5,
320
+ // How many focused facets this Will can hold at once before spawning starts
321
+ // evicting (FacetSupervisor). A structural ceiling, not the live budget:
322
+ // attention scales the allowance *within* it each tick, so a tired or loaded
323
+ // mind narrows on its own. The metacog loop develops it via the persona-prior
324
+ // (openness widens, conscientiousness narrows), which is what makes "how many
325
+ // things I can hold at once" a property of this person rather than a constant.
326
+ maxFacets: 10,
327
+ // How long a QUIET thread stays open before the mind considers it finished
328
+ // (FacetSupervisor idle reaper). The sibling of maxFacets — that one is how
329
+ // many threads at once, this one is how long each survives a silence — and
330
+ // it was the only number in the economy no personality could move.
331
+ //
332
+ // ~30 minutes at a typical tick rate. It was hardcoded at 50 ticks, which is
333
+ // THIRTY SECONDS: every pause longer than a person taking a moment to type
334
+ // destroyed the conversation, and the reply landed on a facet that had never
335
+ // heard of them. Generous is safe — maxFacets + eviction bound the population;
336
+ // this only decides when silence means "over".
337
+ facetIdleTtlTicks: 3000,
319
338
  },
320
339
  },
321
340
  {
@@ -366,6 +385,35 @@ export function buildEngineConfigEntities( config: WillConfig, executiveInterval
366
385
  switchCost: 0.15,
367
386
  riskWeight: 0.20,
368
387
  noveltyWeight: 0.10,
388
+ // How hard an act's own live footprint damps doing it again (EXAFFERENCE
389
+ // P5) — how long this mind sits with something it has already said before
390
+ // saying it again. Agreeableness develops it up, demonstrated persistence
391
+ // down, so "gives people room" vs "chases an answer" is a trait rather
392
+ // than a constant.
393
+ repeatDamping: 0.30,
394
+ // Ticks an act keeps satiating the urge to repeat it. Separate from the
395
+ // consequence TTL on purpose: that one is "how long until the world's echo
396
+ // could still arrive" (short, and about perception), this is "how long
397
+ // before saying it again feels right" (a disposition). Same two traits as
398
+ // repeatDamping move it — patience lengthens, persistence shortens.
399
+ repeatWindowTicks: 60,
400
+ // Ticks before a silence starts to mean something — how long this mind
401
+ // gives someone to get back to it before it counts the turn unanswered
402
+ // and learns from that (conversation.aim / ReafferenceEngine).
403
+ //
404
+ // Lives beside repeatWindowTicks rather than in a config of its own
405
+ // because they are two readings of ONE disposition, and splitting them
406
+ // would let a mind tune itself into contradiction — coming back to
407
+ // something in 20 ticks while still calling the silence too fresh to
408
+ // count. Long relative to its neighbours by design: repeatWindowTicks
409
+ // asks "how long before saying it again feels right", this asks "how long
410
+ // before I take not hearing back as information", and at 1s/tick that is
411
+ // four minutes of a real person's time, not one.
412
+ replyWindowTicks: 240,
413
+ // How much a learned read on someone biases acting toward them. SIGNED and
414
+ // unclamped: a warm mind leans toward whoever answers, a dogged one chases
415
+ // the silence. The container will not choose between those.
416
+ socialWeight: 0.30,
369
417
  },
370
418
  },
371
419
 
@@ -473,3 +521,63 @@ export function buildEngineConfigEntities( config: WillConfig, executiveInterval
473
521
  },
474
522
  ]
475
523
  }
524
+
525
+ // ── The single writer ─────────────────────────────────────────
526
+
527
+ /**
528
+ * Write an `engine.config` entity — MERGING, always. The only sanctioned way to
529
+ * write one; `tests/unit/config.mirror.writer.test.ts` fails on a raw
530
+ * `setEntity({ type: 'engine.config' })` anywhere else.
531
+ *
532
+ * Every whole-entity write to one of these has silently dropped params, three
533
+ * times in one day and each in a different place:
534
+ *
535
+ * • PMALoader replaced `engine-config-executive` with the three behavioural
536
+ * params a PMA carries, dropping `deliberateThreshold` — so `readBaseParams`
537
+ * returned nothing for it and `consolidatePrior` skipped the analytical and
538
+ * decisiveness edges outright, for every Will ever restored from an artifact.
539
+ * • The same loader dropped `emitBlendEvents` from the blender and three params
540
+ * from forgetting.
541
+ * • Snapshot restore replaced the whole mirror, so a Will woke with the config
542
+ * it FIRST hibernated under and could never receive a param added later —
543
+ * `maxFacets` and `deliberateThreshold` were inert on a live Will for its
544
+ * entire life.
545
+ *
546
+ * `precedence` says which side wins on a key both hold. Neither ever drops a key.
547
+ *
548
+ * 'incoming' — the caller is the authority (boot seed; a PMA supplying the
549
+ * tenant's own dispositions). Keys it does not mention survive.
550
+ * 'existing' — state is the authority (post-restore backfill). Only genuinely
551
+ * missing keys are added, so learned and PMA'd values are safe.
552
+ *
553
+ * Returns the keys it actually added or changed, for the caller to log.
554
+ */
555
+ export function mergeEngineConfig(
556
+ store: StateManager,
557
+ cfg: EngineConfigEntity,
558
+ precedence: 'incoming' | 'existing' = 'incoming',
559
+ ): string[] {
560
+ const existing = store.getEntity( cfg.id )
561
+ const current = ( existing?.metadata as { params?: Record<string, unknown> } | undefined )?.params ?? {}
562
+
563
+ const params = precedence === 'existing'
564
+ ? { ...cfg.params, ...current } // state wins; fills only what is missing
565
+ : { ...current, ...cfg.params } // caller wins; keeps everything else
566
+
567
+ const changed = Object.keys( params ).filter( k => params[ k ] !== current[ k ] )
568
+ if( existing && changed.length === 0 ) return []
569
+
570
+ // No timestamps: StateManager.setEntity is the single place they are stamped,
571
+ // and it sources them from the SIM clock so entity times replay identically
572
+ // (R2). It also preserves an existing `createdAt`. The write sites this
573
+ // replaced all passed `Date.now()`, which was both redundant and a real
574
+ // determinism hole — the guard test caught it the moment the code moved into
575
+ // `cognition/`, where wall-clock reads are banned.
576
+ store.setEntity({
577
+ id: cfg.id,
578
+ type: 'engine.config',
579
+ metadata: { engine: cfg.engine, params },
580
+ })
581
+
582
+ return changed
583
+ }
@@ -566,3 +566,25 @@ globalSchemaRegistry.register({
566
566
  return hasNum( p, 'count')
567
567
  },
568
568
  })
569
+
570
+ // ── Deliberation cache (fast-path telemetry) ─────────────────
571
+ // Published from the ExecutiveEngine's committed path (onReasoningComplete),
572
+ // never from inside the pure cache. Lets faculties like the PersonaConsolidator
573
+ // react to how automatic the Will is becoming (e.g. a high hit rate could lower
574
+ // the deliberate-effort threshold).
575
+
576
+ globalSchemaRegistry.register({
577
+ type: 'cache.hit', version: 1,
578
+ validate( p ){
579
+ if( !isObj(p) ) return 'payload must be object'
580
+ return hasNum( p, 'confidence') ?? hasNum( p, 'neighborCount')
581
+ },
582
+ })
583
+
584
+ globalSchemaRegistry.register({
585
+ type: 'cache.miss', version: 1,
586
+ validate( p ){
587
+ if( !isObj(p) ) return 'payload must be object'
588
+ return hasNum( p, 'confidence')
589
+ },
590
+ })
@@ -23,6 +23,7 @@ import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
23
23
  import { GenerativeModel } from '#cognition/generative.model'
24
24
  import { readEffectiveParams } from '#cognition/persona.prior'
25
25
  import { ExecutiveEngine } from '#faculties/executive.engine'
26
+ import { identityCommand } from '#cognition/identity.entity'
26
27
 
27
28
  export interface AutobiographicalNarratorConfig {
28
29
  minIntervalTicks?: number
@@ -171,16 +172,10 @@ export class AutobiographicalNarrator implements SimulationEngine, CognitiveEngi
171
172
  for( const { key: trait, value: delta } of executiveOutput.identityUpdates.traits )
172
173
  updatedTraits[ trait ] = Math.max( 0, Math.min( 1, ( updatedTraits[ trait ] ?? 0.5 ) + delta ) )
173
174
 
174
- commands.set!.push({
175
- id: 'identity-self',
176
- type: 'will.identity',
177
- createdAt: existingIdentity.createdAt,
178
- metadata: {
179
- ...existingIdentity.metadata,
180
- traits: updatedTraits,
181
- version: ( ( existingIdentity.metadata?.version as number ) ?? 1 ) + 1,
182
- },
183
- })
175
+ commands.set!.push( identityCommand( state, {
176
+ traits: updatedTraits,
177
+ version: ( ( existingIdentity.metadata?.version as number ) ?? 1 ) + 1,
178
+ } ) )
184
179
  }
185
180
  }
186
181
 
@@ -114,6 +114,13 @@ export class EpisodicConsolidator implements SimulationEngine, CognitiveEngine {
114
114
 
115
115
  // Vector memory integration
116
116
  private _vectorMemory: VectorMemoryAdapter | null = null
117
+ /**
118
+ * In-flight background indexing. Indexing is deliberately not awaited inside
119
+ * react() (a rate-limit retry chain would stall the whole tick loop), so this is
120
+ * the handle for the two callers that genuinely must wait for it: shutdown,
121
+ * before persisting the index, and tests asserting on it.
122
+ */
123
+ private _indexing: Promise<void> = Promise.resolve()
117
124
  private _embedder: EmbeddingProvider | null = null
118
125
  private _autoIndex: boolean
119
126
 
@@ -278,7 +285,35 @@ export class EpisodicConsolidator implements SimulationEngine, CognitiveEngine {
278
285
  content: ep.content
279
286
  } ) )
280
287
 
281
- await this._vectorMemory.indexBatch( episodesWithContent )
288
+ // Indexing is BEST-EFFORT, exactly like semanticQuery. It calls an embedding
289
+ // provider, so a rate limit or an outage is an ordinary condition, not a
290
+ // cognitive fault: the episode is already consolidated and in the store, and
291
+ // only its vector is missing. Letting that throw took the whole engine down
292
+ // mid-tick — observed live as `Engine "episodic-consolidator" threw at tick
293
+ // 107: Embedding failed: 429`, which also aborted the rest of this react()
294
+ // (store sync, forgetting-curve decay) for that tick.
295
+ //
296
+ // The episode itself is NOT lost: it is in `_store` and is written to state by
297
+ // the periodic full-store sync below, so it survives snapshot/restore. Only its
298
+ // VECTOR is missing, which costs semantic recall of that episode for the rest of
299
+ // the session — `rebuildFromStore()` re-indexes the whole store on a later
300
+ // restore that finds no persisted index. Nothing re-indexes it in-session; that
301
+ // is a known gap, not a claim that this self-heals.
302
+ // NOT awaited. Nothing in this tick reads the vector index, and the embedding
303
+ // call is a network round-trip behind a rate-limit gate whose retry chain runs
304
+ // ~60s (4+8+16+32s) before giving up. Awaiting it made a single 429 stall the
305
+ // WHOLE TICK LOOP for a minute — measured: ticks 3→4 and 4→5 took 64.9s and
306
+ // 63.5s, one per deferred batch. That is not merely slow: `AWAIT_TIMEOUT` and
307
+ // every other agency deadline are denominated in TICKS, so a 60s tick silently
308
+ // turned a 15-tick timeout into 15 minutes, left one communicate intent stuck
309
+ // 'awaiting', and — because the selector is serial — blocked every subsequent
310
+ // action. 45 executive decisions produced 1 intent and 0 delivered messages.
311
+ this._indexing = this._vectorMemory.indexBatch( episodesWithContent ).catch( ( err: unknown ) => {
312
+ logger.warn(
313
+ `[EpisodicConsolidator] indexing deferred for ${ newEpisodes.length } episode(s) — ` +
314
+ `${ err instanceof Error ? err.message : String( err ) }`
315
+ )
316
+ } )
282
317
  }
283
318
 
284
319
  // 5. Periodic full-store sync — captures activationStrength decay (forgetting curve),
@@ -385,6 +420,9 @@ export class EpisodicConsolidator implements SimulationEngine, CognitiveEngine {
385
420
  * Other metadata narrowing (sourceType / tags) remains the caller's job on the
386
421
  * returned episodes (which carry all metadata).
387
422
  */
423
+ /** Await any background indexing still in flight. Shutdown and tests only. */
424
+ async flushIndexing(): Promise<void> { await this._indexing }
425
+
388
426
  async semanticQuery(
389
427
  query: unknown,
390
428
  filters?: {
@@ -417,10 +455,18 @@ export class EpisodicConsolidator implements SimulationEngine, CognitiveEngine {
417
455
  let timer: ReturnType<typeof setTimeout> | undefined
418
456
  const timedOut = Symbol('recall-timeout')
419
457
 
458
+ // The search keeps running after the race is lost — its own rate-limit retry
459
+ // chain can outlive this timeout by a minute. Its rejection is caught HERE
460
+ // rather than left to the race: a promise that loses a Promise.race still
461
+ // settles, and an unhandled 429 rejection surfaces as a process-level warning
462
+ // (or a crash, depending on host) long after the recall it belonged to gave up.
420
463
  const results = await Promise.race( [
421
464
  this._vectorMemory.search( query, {
422
465
  maxResults: fetch,
423
466
  minSimilarity: filters?.minSimilarity,
467
+ } ).catch( ( err: unknown ) => {
468
+ logger.warn(`[EpisodicConsolidator] recall search failed — ${ err instanceof Error ? err.message : String( err ) }`)
469
+ return []
424
470
  } ),
425
471
  new Promise<typeof timedOut>( resolve => { timer = setTimeout( () => resolve( timedOut ), timeoutMs ) } ),
426
472
  ] ).finally( () => clearTimeout( timer ) )
@@ -658,8 +704,18 @@ export class EpisodicConsolidator implements SimulationEngine, CognitiveEngine {
658
704
  if( this._vectorMemory ){
659
705
  await this._vectorMemory.load()
660
706
  if( this._vectorMemory.size === 0 && this._store.length > 0 ){
661
- await this._vectorMemory.rebuildFromStore( this._store )
662
- logger.info(`[episodic] vector index rebuilt with ${this._store.length} episodes`)
707
+ // Re-embedding the whole store is a bulk network operation, and this runs on
708
+ // the FIRST TICK after restore. Awaited, it held tick 1 for 66.7s against a
709
+ // 500ms budget and threw on a rate limit — a mind spending its first waking
710
+ // minute frozen, with every tick-denominated deadline stretched around it.
711
+ // Detached: recall degrades to "not yet indexed" until it lands, which is the
712
+ // same best-effort contract semanticQuery already has. `flushIndexing()` is
713
+ // what shutdown drains, so a rebuild in flight is still written out.
714
+ this._indexing = this._vectorMemory.rebuildFromStore( this._store )
715
+ .then( () => { logger.info(`[episodic] vector index rebuilt with ${ this._store.length } episodes`) } )
716
+ .catch( ( err: unknown ) => {
717
+ logger.warn(`[episodic] vector index rebuild deferred — ${ err instanceof Error ? err.message : String( err ) }`)
718
+ } )
663
719
  } else if( this._vectorMemory.size > 0 ){
664
720
  logger.info(`[episodic] vector index loaded from disk (${this._vectorMemory.size} entries)`)
665
721
  }
@@ -9,6 +9,9 @@ import type { ExecutiveSummarizer } from '#llm/summarizer'
9
9
  import type { GoalManager } from '#faculties/goal.manager'
10
10
  import type { GenerativeModel } from '#cognition/generative.model'
11
11
  import type { SemanticIntegrator } from '#faculties/semantic.engine/integrator'
12
+ import { INNATE_SCHEMA_BY_ID } from '#agency/schemas/innate'
13
+ import { logger } from '#core/logger'
14
+ import { resolveKeid } from '#cognition/social.identity'
12
15
 
13
16
  /** Maps the LLM's evidence enum to a numeric supportingEpisodes value for the belief store. */
14
17
  export const EVIDENCE_TO_COUNT: Record<string, number> = {
@@ -168,10 +171,13 @@ export function buildStateCommands(
168
171
  supportingEpisodes: belief.supportingEpisodes, tags: belief.tags } })
169
172
  })
170
173
 
171
- if( bus && ( u.name || u.feeling != null ) )
174
+ if( bus && ( u.name || u.feeling != null || u.sameAs ) )
172
175
  effects.push( () => bus.publish({
173
176
  type: 'known.entity.learned', version: 1, sourceEngine: 'executive',
174
- salience: 0.5, payload: { keid: u.keid, name: u.name, feeling: u.feeling },
177
+ // An identity verdict is worth more attention than a learned name: it
178
+ // reorganises everything the mind holds about two referents at once.
179
+ salience: u.sameAs ? 0.7 : 0.5,
180
+ payload: { keid: u.keid, name: u.name, feeling: u.feeling, sameAs: u.sameAs },
175
181
  }) )
176
182
  })
177
183
  }
@@ -321,6 +327,32 @@ export function publishCognitiveEvents(
321
327
  }
322
328
  })
323
329
 
330
+ // agency.composite.proposed — the mind names a compound action as one skill.
331
+ //
332
+ // This is the creation seam for the instrumental→habitual gradient, and it had
333
+ // no producer: ReafferenceEngine subscribes to this event and its handler is the
334
+ // ONLY caller of SchemaRepertoire.registerComposite() anywhere in the tree, so
335
+ // until now no Will could hold a skill beyond the innate floor, for its entire
336
+ // life (#114). A capability the container offered and no tenant could reach.
337
+ //
338
+ // One event per proposal — the consumer registers them individually and drops
339
+ // anything with fewer than two sub-schemas, since a "composite" of one is just
340
+ // the schema it already had.
341
+ for( const skill of output.newSkills ?? [] )
342
+ bus.publish({
343
+ type: 'agency.composite.proposed',
344
+ version: 1,
345
+ sourceEngine: 'executive-engine',
346
+ salience: 0.7,
347
+ payload: {
348
+ id: skill.id,
349
+ composedOf: skill.composedOf,
350
+ ...( skill.tags ? { tags: skill.tags } : {} ),
351
+ ...( typeof skill.cost === 'number' ? { cost: skill.cost } : {} ),
352
+ tick: footprint.tickObserved,
353
+ }
354
+ })
355
+
324
356
  // goal.proposed — when executive proposes new goals
325
357
  if( output.newGoals?.length )
326
358
  bus.publish({
@@ -372,20 +404,45 @@ export function publishCognitiveEvents(
372
404
  })
373
405
  }
374
406
 
375
- const COMMUNICATE_ACTION_TYPES = new Set([
407
+ /**
408
+ * Action names that mean "say something to someone". Exported because the
409
+ * conversation facet partitions its OWN actions by the same rule (an action
410
+ * aimed at a third party is an intention the master owns, not a reply the facet
411
+ * may deliver) — one definition, so the two ends cannot drift apart.
412
+ */
413
+ export const COMMUNICATE_ACTION_TYPES = new Set([
376
414
  'communicate', 'speak', 'initiate_conversation', 'reach-out', 'reach_out', 'talk', 'text', 'message',
377
415
  ])
378
416
 
417
+ /** Arg keys a mind uses for the words themselves — folded into `gist` (see below). */
418
+ const WORDS_ARG_KEYS = new Set([ 'content', 'message', 'text', 'body' ])
419
+ /** Arg keys naming the addressee — already resolved into `targetEntityId`. */
420
+ const ADDRESS_ARG_KEYS = new Set([ 'to', 'recipient', 'target', 'targetEntityId', 'entityId' ])
421
+
379
422
  /** Resolve an executive action target (a display name OR a keid) to a known-entity keid. */
380
423
  function resolveKnownEntity( target: string, state: ReadonlySimulationState ): string | undefined {
381
- const t = target.trim().toLowerCase()
424
+ // THE resolver, shared with everything else that has to turn a reference into a
425
+ // referent. This was a private scan matching an exact keid or an exact name and
426
+ // consulting no alias table — so once dossiers were keyed by an anchor, naming
427
+ // someone by the address they were met at (`discord:1019…`) matched nothing and
428
+ // the whole intention evaporated silently, which is the failure this function's
429
+ // own logging exists to make visible.
430
+ return resolveKeid( state.entities as never, target )
431
+ }
432
+
433
+ /**
434
+ * The name the mind has LEARNED for this entity. The outreach facet addresses someone
435
+ * by it ("I have decided to reach out to ${ name }"), so a keid leaking through here
436
+ * would have the mind reaching out to `discord:1019376031150379101`. Undefined when
437
+ * unlearned — the caller omits it rather than substituting a placeholder.
438
+ */
439
+ function knownEntityName( keid: string, state: ReadonlySimulationState ): string | undefined {
382
440
  for( const e of state.entities.values() ){
383
441
  if( e.type !== 'known-entity') continue
384
- const m = e.metadata as Record<string, unknown> | undefined
385
- const keid = typeof m?.['keid'] === 'string' ? m['keid'] as string : undefined
386
- const name = typeof m?.['name'] === 'string' ? m['name'] as string : undefined
387
- if( keid && keid.toLowerCase() === t ) return keid
388
- if( name && name.toLowerCase() === t ) return keid
442
+ const m = e.metadata as Record<string, unknown> | undefined
443
+ if( m?.['keid'] !== keid ) continue
444
+ const name = typeof m['name'] === 'string' ? m['name'].trim() : ''
445
+ return name.length > 0 ? name : undefined
389
446
  }
390
447
  return undefined
391
448
  }
@@ -408,6 +465,10 @@ function buildIdeomotorIntents(
408
465
  ): { set: EntityInput[]; delete: string[] } {
409
466
  const set: EntityInput[] = []
410
467
  const seen = new Set<string>()
468
+ /** Action names that named nothing this cycle — surfaced back to the mind below. */
469
+ const unresolved = new Set<string>()
470
+ /** Addressees the mind meant to reach but cannot resolve to anyone it knows. */
471
+ const unaddressed = new Set<string>()
411
472
  const priority = clamp01( output.confidence ?? 0.8 )
412
473
 
413
474
  // The host abilities currently afforded (source 'external' in the live field) —
@@ -425,21 +486,83 @@ function buildIdeomotorIntents(
425
486
  const t = action.type.toLowerCase()
426
487
 
427
488
  if( COMMUNICATE_ACTION_TYPES.has( t ) ){
428
- if( !action.target ) continue
429
- const keid = resolveKnownEntity( action.target, state )
430
- if( !keid || seen.has( keid ) ) continue
489
+ // The addressee may be named on the action OR inside the args the executive
490
+ // authored. This used to read `action.target` alone and `continue` when it
491
+ // was absent but the output guidelines document actions as
492
+ // `{type, reasoning, expectedOutcome}` and tell the mind to put specifics
493
+ // in `args`, so `args.to` is precisely what a well-behaved mind produces.
494
+ // A Will would write real sentences into `args.to`/`args.content`, the
495
+ // intent was never created, nothing competed, nothing was ever enqueued —
496
+ // and reafference then taught it that talking to that PERSON does not work.
497
+ const args = ( action.args && typeof action.args === 'object' ? action.args : {} ) as Record<string, unknown>
498
+ const named = [ action.target, args['to'], args['recipient'], args['target'], args['targetEntityId'], args['entityId'] ]
499
+ .find( v => typeof v === 'string' && v.trim().length > 0 ) as string | undefined
500
+ // Naming NOBODY and naming someone unreachable are different failures, and
501
+ // both used to `continue` in silence — the intent was never created, nothing
502
+ // competed, and the mind had no way to find out. Observed: a facet decided to
503
+ // contact a colleague by a name the mind had heard in conversation but never
504
+ // bound to a dossier; the whole intention evaporated without a trace, and the
505
+ // person it had just promised never heard from it.
506
+ if( !named ){ unaddressed.add('(no one)'); continue }
507
+ const keid = resolveKnownEntity( named, state )
508
+ if( !keid ){ unaddressed.add( named ); continue }
509
+ if( seen.has( keid ) ) continue
431
510
  seen.add( keid )
511
+ // The master forms the INTENT; it does not author the words. Whatever it wrote
512
+ // is the DIRECTION for the outreach facet (AuditionEngine.authorOutreach) to
513
+ // speak in — so it lands in `gist`, never `content`. This is load-bearing:
514
+ // MotorSchemaExecutor._deliver sends `parameters.content` VERBATIM and only
515
+ // falls back to the facet when it is empty, so carrying the master's sentences
516
+ // as `content` would put the master itself in a second, parallel conversation
517
+ // with someone a conversation facet may already be talking to — one mind
518
+ // holding two threads with one person about one thing. `gist` was read in
519
+ // three places and written nowhere; this is what writes it.
520
+ const said = [ ...WORDS_ARG_KEYS ]
521
+ .map( k => args[ k ] )
522
+ .find( v => typeof v === 'string' && v.trim().length > 0 ) as string | undefined
523
+ const parameters: Record<string, unknown> = {}
524
+ for( const [ k, v ] of Object.entries( args ) )
525
+ if( !WORDS_ARG_KEYS.has( k ) && !ADDRESS_ARG_KEYS.has( k ) ) parameters[ k ] = v
526
+ if( said ) parameters['gist'] = said
527
+ const targetName = knownEntityName( keid, state )
528
+ if( targetName ) parameters['targetEntityName'] = targetName
529
+
530
+ // Named at INFO because this is the seam where a decision to contact someone
531
+ // either becomes a competing intention or disappears. When a Will named a
532
+ // colleague seven times over ten minutes and he never heard from it, nothing
533
+ // in the logs could distinguish "the intent was never created" from "it was
534
+ // created and lost every competition" — the two have completely different
535
+ // fixes, and the archaeology to tell them apart needed state snapshots that
536
+ // sample too coarsely to catch a cycle.
537
+ logger.info(
538
+ `[executive] willed reach-out → ${ targetName ?? keid } ` +
539
+ `(named '${ named }' → ${ keid }, priority=${ priority.toFixed( 2 ) })`
540
+ )
541
+
432
542
  set.push({
433
543
  id: `ideomotor-reach-out-${ keid }`,
434
544
  type: 'ideomotor.intent',
435
- metadata: { schema: 'reach-out', targetEntityId: keid, priority, origin: 'executive', tick: footprint.tickObserved },
545
+ metadata: {
546
+ schema: 'reach-out', targetEntityId: keid,
547
+ ...( Object.keys( parameters ).length > 0 ? { parameters } : {} ),
548
+ priority, origin: 'executive', tick: footprint.tickObserved,
549
+ },
436
550
  })
437
551
  continue
438
552
  }
439
553
 
440
554
  // A host ability the executive imagines enacting, with its conscious args.
441
555
  const schema = externalBySchema.get( t )
442
- if( !schema || seen.has(`ability:${ schema }`) ) continue
556
+ if( !schema ){
557
+ // The name resolves to NOTHING: not a communicate type, not an innate
558
+ // stance, not an ability the field affords. Record it so the mind can find
559
+ // out. Silence here is what let a Will spend eleven consecutive actions on
560
+ // an invented `query`, observe that nothing ever came of them, and conclude
561
+ // its MEMORY was broken — the one explanation that was not true.
562
+ if( !INNATE_SCHEMA_BY_ID.has( t ) ) unresolved.add( action.type )
563
+ continue
564
+ }
565
+ if( seen.has(`ability:${ schema }`) ) continue
443
566
  seen.add(`ability:${ schema }`)
444
567
  const keid = action.target ? resolveKnownEntity( action.target, state ) : undefined
445
568
  set.push({
@@ -454,6 +577,51 @@ function buildIdeomotorIntents(
454
577
  })
455
578
  }
456
579
 
580
+ // An action that named nothing is REPORTED, not swallowed.
581
+ //
582
+ // The executive's actions bias the agency competition; they are not commands, so
583
+ // a name that matches no schema is not a dispatch error and nothing downstream
584
+ // ever objected. But an unopposed no-op is indistinguishable from an act that
585
+ // was tried and achieved nothing, and the mind reasons from the difference.
586
+ // Observed: eleven consecutive `query` actions (a name that does not exist),
587
+ // then "five consecutive queries with no memory trace is a failure mode" and a
588
+ // plan to diagnose its own memory. Telling it the name was not real costs one
589
+ // entity and removes a whole class of false self-belief.
590
+ if( unresolved.size > 0 )
591
+ set.push({
592
+ id: 'action.unresolved',
593
+ type: 'action.unresolved',
594
+ metadata: {
595
+ names: [ ...unresolved ],
596
+ summary: `I named ${ [ ...unresolved ].map( n => `'${ n }'` ).join(', ') } as an action, but ${ unresolved.size > 1 ? 'those are not things' : 'that is not a thing' } I can do — no such ability is in my repertoire or afforded right now. Nothing happened. To act I have to name something I actually have.`,
597
+ salience: 0.75,
598
+ origin: 'executive',
599
+ tick: footprint.tickObserved,
600
+ },
601
+ })
602
+
603
+ // An addressee that resolves to nobody is REPORTED, not swallowed.
604
+ //
605
+ // Same principle as the unresolved-name report above, one layer down: there the
606
+ // *verb* named nothing, here the *person* does. The mind can hear a name in
607
+ // conversation ("coordinate that through FKEM") long before that name is bound
608
+ // to anyone it can actually reach, and reaching-out to an unbound name simply
609
+ // does not happen. Told, it can do the human thing — ask how to reach them, or
610
+ // ask whoever mentioned them to make the introduction. Untold, it believes it
611
+ // made contact and follows up on a message it never sent.
612
+ if( unaddressed.size > 0 )
613
+ set.push({
614
+ id: 'action.unaddressed',
615
+ type: 'action.unaddressed',
616
+ metadata: {
617
+ names: [ ...unaddressed ],
618
+ summary: `I meant to reach ${ [ ...unaddressed ].map( n => `'${ n }'` ).join(', ') }, but ${ unaddressed.size > 1 ? 'those names match no one' : 'that name matches no one' } I know how to contact — no message went out. If I want to reach them I need a way to: someone can introduce us, or tell me where to find them.`,
619
+ salience: 0.8,
620
+ origin: 'executive',
621
+ tick: footprint.tickObserved,
622
+ },
623
+ })
624
+
457
625
  // Clear stale executive-sourced intents the executive no longer imagines this cycle.
458
626
  const currentIds = new Set( set.map( s => s.id ) )
459
627
  const del: string[] = []
@@ -463,6 +631,13 @@ function buildIdeomotorIntents(
463
631
  && !currentIds.has( id ) )
464
632
  del.push( id )
465
633
 
634
+ // Clear the report once the mind names only real actions again — it should read
635
+ // as "that last attempt was not a thing", not as a permanent defect in itself.
636
+ if( unresolved.size === 0 && state.entities.has('action.unresolved') )
637
+ del.push('action.unresolved')
638
+ if( unaddressed.size === 0 && state.entities.has('action.unaddressed') )
639
+ del.push('action.unaddressed')
640
+
466
641
  return { set, delete: del }
467
642
  }
468
643