@mindot/will 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +87 -22
  2. package/dist/channels/discord.d.ts +67 -6
  3. package/dist/channels/discord.js +112 -6
  4. package/dist/channels/discord.js.map +1 -1
  5. package/dist/channels/whatsapp.d.ts +1 -1
  6. package/dist/channels/whatsapp.js +4 -1
  7. package/dist/channels/whatsapp.js.map +1 -1
  8. package/dist/cli.js +13733 -11057
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +3729 -1199
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/effectors.d.ts +1 -1
  14. package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
  15. package/package.json +1 -1
  16. package/src/channels/discord.ts +189 -11
  17. package/src/channels/types.ts +90 -0
  18. package/src/channels/whatsapp.ts +13 -4
  19. package/src/cli.ts +9 -4
  20. package/src/cognition/agency/consequence.ts +122 -1
  21. package/src/cognition/agency/conversation.aim.ts +260 -0
  22. package/src/cognition/agency/engines/action.selector.ts +85 -3
  23. package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
  24. package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
  25. package/src/cognition/agency/engines/reafference.engine.ts +129 -2
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +16 -2
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/repertoire.ts +12 -5
  30. package/src/cognition/agency/selection.scoring.ts +33 -0
  31. package/src/cognition/agency/types.ts +35 -0
  32. package/src/cognition/cache/composition.ts +232 -0
  33. package/src/cognition/cache/deliberation.cache.ts +219 -0
  34. package/src/cognition/cache/fingerprint.ts +120 -0
  35. package/src/cognition/cache/types.ts +105 -0
  36. package/src/cognition/config.mirror.entities.ts +109 -1
  37. package/src/cognition/event.schemas.ts +22 -0
  38. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  39. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  40. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  41. package/src/cognition/faculties/executive.engine/context.ts +67 -13
  42. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  43. package/src/cognition/faculties/executive.engine/engine.ts +686 -187
  44. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
  45. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
  46. package/src/cognition/faculties/executive.engine/facet.ts +89 -26
  47. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  48. package/src/cognition/faculties/executive.engine/parser.ts +21 -1
  49. package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
  50. package/src/cognition/faculties/executive.engine/types.ts +69 -0
  51. package/src/cognition/faculties/goal.manager.ts +94 -14
  52. package/src/cognition/faculties/known.entity.tracker.ts +267 -28
  53. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  54. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  55. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  56. package/src/cognition/faculties/self.model.updater.ts +19 -12
  57. package/src/cognition/faculties/social.perception.ts +47 -3
  58. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  59. package/src/cognition/faculties/working.memory.ts +10 -20
  60. package/src/cognition/identity.entity.ts +205 -0
  61. package/src/cognition/index.ts +11 -0
  62. package/src/cognition/memory/vector.adapter.ts +12 -3
  63. package/src/cognition/memory/vector.embedder.ts +54 -7
  64. package/src/cognition/persona.prior.ts +6 -0
  65. package/src/cognition/senses/audition.engine/engine.ts +404 -46
  66. package/src/cognition/senses/base.sense.engine.ts +1 -1
  67. package/src/cognition/senses/index.ts +12 -0
  68. package/src/cognition/social.identity.ts +273 -0
  69. package/src/cognition/utilities/token.tracker.ts +246 -98
  70. package/src/core/orchestrator.ts +38 -0
  71. package/src/host/boot.ts +78 -22
  72. package/src/index.ts +35 -0
  73. package/src/llm/index.ts +415 -97
  74. package/src/llm/routing.ts +204 -0
  75. package/src/llm/summarizer.ts +5 -1
  76. package/src/llm/wire.contracts.ts +19 -0
  77. package/src/pma/index.ts +67 -53
  78. package/src/runners/thin-shim.runner.ts +18 -6
  79. package/src/sdk/will.ts +121 -22
  80. package/src/stem/assembly.audit.ts +1 -0
  81. package/src/stem/guards/identity.coherence.ts +17 -6
  82. package/src/stem/index.ts +82 -5
  83. package/src/stem/mind.ts +327 -79
  84. package/src/stem/policy/arbiter.ts +49 -14
  85. package/src/stem/policy/rule.table.ts +2 -2
  86. package/src/stem/tracts/effector.controller.ts +56 -9
  87. package/src/stem/tracts/outbox.writer.ts +40 -2
  88. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
@@ -37,6 +37,9 @@ import type { CognitiveEventSchema } from '#cognition/schema.registry'
37
37
  import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
38
38
  import type { Percept } from '#senses/index'
39
39
  import { GenerativeModel } from '#cognition/generative.model'
40
+ import {
41
+ isReferentId, mintReferentId, canonicalOf, withHandle, type Handle,
42
+ } from '#cognition/social.identity'
40
43
  import { readEffectiveParams } from '#cognition/persona.prior'
41
44
 
42
45
  export interface KnownEntityTrackerConfig {
@@ -71,6 +74,31 @@ export interface KnownEntity {
71
74
  lastSeenTick: Tick
72
75
  /** 0–1: how identified/coherent this referent is (a name + repeated encounters raise it). */
73
76
  resolutionConfidence: number
77
+ /**
78
+ * The ways this referent has been reachable, with the circumstances.
79
+ *
80
+ * Distinct from an ALIAS, and the distinction is load-bearing: an alias is
81
+ * another NAME for the same referent (a transport user id), a handle is a
82
+ * PLACE it can be reached (a thread, a room). One person has one identity and
83
+ * several rooms, and conflating them is what made "which room should I say
84
+ * this in?" unaskable.
85
+ */
86
+ handles: Handle[]
87
+ /**
88
+ * Referents this one MIGHT be the same someone as, unresolved.
89
+ *
90
+ * A blocked merge used to vanish. `_recognise` will only absorb a THIN handle
91
+ * into an established relationship — rightly, because fusing two real people
92
+ * who share a name is the dangerous direction — so once the same human was
93
+ * well-established on two channels they stayed two people permanently, and
94
+ * nothing anywhere recorded the near-miss.
95
+ *
96
+ * A person does not silently fail here. They NOTICE — "hang on, is this the
97
+ * same Mara?" — and then resolve it by asking. So the doubt is kept, shown, and
98
+ * left for the mind to settle. Deliberately not a merge: this is a question,
99
+ * and the answer is the mind's to give.
100
+ */
101
+ suspectedSameAs?: string[]
74
102
  }
75
103
 
76
104
  /** Percept domains whose entities are minds. Everything else defaults to a thing. */
@@ -85,6 +113,19 @@ const CURIOUS_RESOLVED = 0.6
85
113
  // Forgetting (Phase 4). (Reliability rate is now a Channel-A developable field — analytical.)
86
114
  const FORGET_FLOOR = 0.02 // below this familiarity, an unidentified blip is forgotten
87
115
 
116
+ /**
117
+ * Familiarity decay for a referent the mind has NOT resolved — no name, barely
118
+ * identified. Two orders of magnitude faster than for a known someone, because
119
+ * one rate cannot serve both: a face glimpsed once in a crowd should fade in
120
+ * minutes, and a colleague should not.
121
+ *
122
+ * The forgetting gate already drew exactly this line (`!d.name &&
123
+ * resolutionConfidence < CURIOUS_RESOLUTION`); only the rate did not, so slowing
124
+ * decay enough for relationships to exist would have left every blip resident
125
+ * forever. Same value the single shared rate used to have.
126
+ */
127
+ const BLIP_DECAY_RATE = 0.005
128
+
88
129
  // Recognition (Phase 5) — guards against conflating two *different* people who share a
89
130
  // name. Only recognise a still-thin handle (≤) into a known person, and never when both
90
131
  // were active at the same time (two people talking at once ⇒ distinct, not one on two
@@ -109,18 +150,50 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
109
150
  private _restored = false
110
151
 
111
152
  // Buffered from senses.*.percept events; drained each tick in react().
112
- private _pendingEncounters: Array<{ keid: string; domain: string; name?: string }> = []
153
+ private _pendingEncounters: Array<{
154
+ keid: string; domain: string; name?: string
155
+ /** The room this encounter happened in, and whether it was a private one. */
156
+ thread?: string; direct?: boolean
157
+ }> = []
113
158
  // Buffered from known.entity.learned (the conscious / reasoning write-path, Phase 2.2).
114
- private _pendingConscious: Array<{ keid: string; name?: string; feeling?: number }> = []
159
+ private _pendingConscious: Array<{
160
+ keid: string; name?: string; feeling?: number
161
+ /** "This is the same someone as that." The mind settling an identity itself. */
162
+ sameAs?: string
163
+ }> = []
115
164
  // Buffered from action.outcome — the reliability track-record signal (Phase 4).
116
165
  private _pendingOutcomes: Array<{ keid: string; signal: number }> = []
117
166
 
167
+ /**
168
+ * Aliases minted this tick, awaiting persistence. `_getOrCreate` runs deep inside
169
+ * the drain loops with no access to the command list, and an alias that lives
170
+ * only in memory is one the mind forgets on restart — every transport address
171
+ * would mint a SECOND anchor next boot and the person would fork in two.
172
+ */
173
+ private _mintedAliases: Array<{ alias: string; canonical: string }> = []
174
+
118
175
  private _bus: CognitiveBus | null = null
119
176
  private readonly _model = new GenerativeModel()
120
177
 
121
178
  constructor( config: KnownEntityTrackerConfig = {} ){
122
179
  this._growthRate = config.familiarityGrowthRate ?? 0.15
123
- this._decayRate = config.familiarityDecayRate ?? 0.005
180
+ // 0.005/tick spends the entire scale in 200 ticks — under four minutes at the
181
+ // 1s tick a hosted Will actually runs on. Familiarity could therefore never
182
+ // accumulate for a mind whose encounters are minutes apart, and it did not:
183
+ // measured 0.00 on a fresh mind after 5 encounters, and 0.00 on a mind that
184
+ // had banked 71. A permanently-zero signal is worse than a missing one — it
185
+ // feeds the curiosity-to-resolve drive (which therefore never fired) and the
186
+ // pruning order, both of which read it as "a total stranger".
187
+ //
188
+ // Mere exposure fades over days, not minutes. At 0.00002 a full scale takes
189
+ // ~50k ticks (~14h at 1s), so someone met this morning is still familiar
190
+ // tonight and a stranger from last month has genuinely faded.
191
+ //
192
+ // The deeper flaw stays: this is per-TICK, so a psychological timescale is
193
+ // pinned to a host's tick rate. `react()` already receives the real delta —
194
+ // expressing decay in experienced time is the actual fix, and it belongs with
195
+ // the other per-tick decays rather than in this one engine alone.
196
+ this._decayRate = config.familiarityDecayRate ?? 0.00002
124
197
  this._curiosityGain = config.curiosityGain ?? 1.0
125
198
  this._reliabilityRate = config.reliabilityRate ?? 0.2
126
199
  this._maxTracked = config.maxTracked ?? 50
@@ -150,9 +223,9 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
150
223
 
151
224
  // Conscious learning (the reasoning write-path): a learned name + felt valence.
152
225
  if( e.type === 'known.entity.learned'){
153
- const u = e.payload as { keid?: string; name?: string; feeling?: number }
226
+ const u = e.payload as { keid?: string; name?: string; feeling?: number; sameAs?: string }
154
227
  if( u?.keid && u.keid !== 'agent-self')
155
- this._pendingConscious.push({ keid: u.keid, name: u.name, feeling: u.feeling })
228
+ this._pendingConscious.push({ keid: u.keid, name: u.name, feeling: u.feeling, sameAs: u.sameAs })
156
229
  return
157
230
  }
158
231
 
@@ -170,10 +243,16 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
170
243
  if( !keid || keid === 'agent-self') return
171
244
 
172
245
  // A channel-supplied display name rides on the raw input (e.g. TextMessage.speakerName).
173
- const raw = p?.raw as { speakerName?: unknown } | undefined
246
+ // A channel supplies the display name AND the room, both on the raw input.
247
+ // `direct` is the one bit the Discord edge computed (`isDM`) and threw away
248
+ // before the mind could see it — the single fact that decides whether a room
249
+ // is the right place for a given utterance.
250
+ const raw = p?.raw as { speakerName?: unknown; threadId?: unknown; direct?: unknown } | undefined
174
251
  const name = typeof raw?.speakerName === 'string' ? raw.speakerName : undefined
252
+ const thread = typeof raw?.threadId === 'string' ? raw.threadId : undefined
253
+ const direct = typeof raw?.direct === 'boolean' ? raw.direct : undefined
175
254
 
176
- this._pendingEncounters.push({ keid, domain: p!.domain, name })
255
+ this._pendingEncounters.push({ keid, domain: p!.domain, name, thread, direct })
177
256
  }
178
257
 
179
258
  snapshot(): Record<string, unknown> {
@@ -200,8 +279,10 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
200
279
 
201
280
  // Familiarity fades a little each tick — absence dims the sense of an entity.
202
281
  for( const d of this._dossiers.values() )
203
- if( d.familiarity > 0 )
204
- d.familiarity = Math.max( 0, d.familiarity - this._decayRate )
282
+ if( d.familiarity > 0 ){
283
+ const unresolved = !d.name && d.resolutionConfidence < CURIOUS_RESOLUTION
284
+ d.familiarity = Math.max( 0, d.familiarity - ( unresolved ? BLIP_DECAY_RATE : this._decayRate ) )
285
+ }
205
286
 
206
287
  // Drain encounters — each percept is an exposure that re-warms familiarity.
207
288
  let touched = false
@@ -210,6 +291,37 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
210
291
  d.encounterCount += 1
211
292
  d.familiarity = Math.min( 1, d.familiarity + this._growthRate * ( 1 - d.familiarity ) )
212
293
  d.lastSeenTick = tick
294
+ // Where this happened is part of what happened. Merged, not appended:
295
+ // meeting someone again in a room already known is news about that room,
296
+ // not a new way to reach them.
297
+ if( enc.thread ){
298
+ d.handles = withHandle( d.handles, {
299
+ keid: enc.thread,
300
+ kind: enc.direct === true ? 'dm' : enc.direct === false ? 'room' : 'unknown',
301
+ lastSeenTick: tick,
302
+ } as Handle )
303
+
304
+ // The ROOM is a referent too, and gets a dossier of its own.
305
+ //
306
+ // `kind: 'thing'` has existed since this tracker shipped and nothing had
307
+ // ever created one — SENTIENT_DOMAINS admits only audition, so the seat
308
+ // for a non-person was built and left empty. A place earns exactly the
309
+ // same things a person does: familiarity with the room, a felt valence
310
+ // toward it, and a reliability that means something real — whether the
311
+ // mind gets answered there. A shared room nobody replies in is a fact
312
+ // about the ROOM as much as about the people in it, and until now there
313
+ // was nowhere to put it.
314
+ //
315
+ // Private threads are deliberately excluded: a DM is not a place, it is
316
+ // the person. Giving it a dossier would double every someone.
317
+ if( enc.direct === false ){
318
+ const place = this._getOrCreate( enc.thread, 'place', tick )
319
+ place.encounterCount += 1
320
+ place.familiarity = Math.min( 1, place.familiarity + this._growthRate * ( 1 - place.familiarity ) )
321
+ place.lastSeenTick = tick
322
+ place.resolutionConfidence = this._resolution( place )
323
+ }
324
+ }
213
325
  if( enc.name && !d.name ) d.name = enc.name // learn a name the channel offers
214
326
  d.resolutionConfidence = this._resolution( d )
215
327
  touched = true
@@ -219,6 +331,16 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
219
331
  // toward, not slammed). A known someone can exist here with no perceptual encounter.
220
332
  for( const u of this._pendingConscious.splice( 0 ) ){
221
333
  const d = this._getOrCreate( u.keid, 'audition', tick ) // a known *someone*
334
+
335
+ // "These two are the same person." The mind settling a doubt the heuristic
336
+ // was not entitled to settle — it has reasons a name-match does not, usually
337
+ // because somebody just told it. Allowed to fuse two ESTABLISHED referents,
338
+ // which is exactly what `_recognise` refuses to do on its own.
339
+ if( u.sameAs ){
340
+ const other = this._dossiers.get( canonicalOf( this._aliases, u.sameAs ) )
341
+ if( other && other.keid !== d.keid ) this._fuse( d, other, commands )
342
+ }
343
+
222
344
  if( u.name ) d.name = u.name
223
345
  if( u.feeling != null )
224
346
  d.valence = Math.max( -1, Math.min( 1, d.valence + 0.5 * ( u.feeling - d.valence ) ) )
@@ -230,7 +352,12 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
230
352
  // performs when acted on (general: a tool, a place, or a person). Only updates an
231
353
  // already-known referent (acting on something you've never perceived doesn't conjure it).
232
354
  for( const o of this._pendingOutcomes.splice( 0 ) ){
233
- const d = this._dossiers.get( o.keid )
355
+ // Through the alias table: an `action.outcome` names its target the way the
356
+ // ACTOR did — a transport address, or whatever the host called it — and the
357
+ // dossier lives under the anchor. Reading `_dossiers` raw here silently
358
+ // dropped every outcome once addresses became aliases, so a referent could
359
+ // never earn or lose reliability again.
360
+ const d = this._dossiers.get( canonicalOf( this._aliases, o.keid ) )
234
361
  if( !d ) continue
235
362
  d.reliability = Math.max( 0, Math.min( 1, d.reliability + this._reliabilityRate * ( o.signal - d.reliability ) ) )
236
363
  touched = true
@@ -255,6 +382,11 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
255
382
  commands.delete!.push(`ke-${d.keid}`)
256
383
  }
257
384
 
385
+ // Persist anchors minted this tick BEFORE the dossiers that depend on them.
386
+ for( const { alias, canonical } of this._mintedAliases.splice( 0 ) )
387
+ commands.set!.push({ id: `kea-${ alias }`, type: 'known-entity-alias',
388
+ metadata: { aliasKeid: alias, canonicalKeid: canonical } })
389
+
258
390
  // Persist dossiers (the perceptual layer of the known-entity node).
259
391
  for( const d of this._dossiers.values() ){
260
392
  if( d.encounterCount === 0 && !d.name ) continue // keep a named-but-unseen someone
@@ -271,6 +403,8 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
271
403
  encounterCount: d.encounterCount,
272
404
  lastSeenTick: d.lastSeenTick,
273
405
  resolutionConfidence: d.resolutionConfidence,
406
+ handles: d.handles,
407
+ ...( d.suspectedSameAs?.length ? { suspectedSameAs: d.suspectedSameAs } : {} ),
274
408
  },
275
409
  })
276
410
  }
@@ -329,10 +463,66 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
329
463
  // ── Public API ───────────────────────────────────────────
330
464
 
331
465
  /** The dossier for a referent, if the Will has one. */
332
- getDossier( keid: string ): KnownEntity | undefined { return this._dossiers.get( keid ) }
466
+ /**
467
+ * The dossier for anything that names this referent — its anchor, or any address
468
+ * it has been met at.
469
+ *
470
+ * Alias-aware because a caller has no business knowing the anchor. This read
471
+ * `_dossiers.get(keid)` raw, so the moment addresses became aliases of a minted
472
+ * anchor, every caller holding a transport id got `undefined` — the mind would
473
+ * have looked up someone it knows perfectly well and found a stranger. The
474
+ * existing Phase-4 tests caught it, which is exactly what they are for.
475
+ */
476
+ getDossier( keid: string ): KnownEntity | undefined {
477
+ return this._dossiers.get( canonicalOf( this._aliases, keid ) )
478
+ }
333
479
 
334
480
  // ── Internal ─────────────────────────────────────────────
335
481
 
482
+ /**
483
+ * Absorb `alias` into `canon` — one someone where there were two.
484
+ *
485
+ * Shared by the recognition heuristic and by the mind's own `sameAs` verdict,
486
+ * because "these are the same person" must mean the same thing whichever
487
+ * concluded it. The only difference is who is ENTITLED to conclude it: the
488
+ * heuristic will not absorb an established relationship, the mind may, because
489
+ * it has reasons a name-match does not — usually that somebody just told it.
490
+ *
491
+ * Routes MOVE. This used to delete the absorbed dossier and keep only a
492
+ * redirect, so the mind concluded "same person" and in the same breath threw
493
+ * away the second way to reach them.
494
+ */
495
+ private _fuse( canon: KnownEntity, alias: KnownEntity, commands: StateCommands ): void {
496
+ canon.encounterCount += alias.encounterCount
497
+ canon.familiarity = Math.max( canon.familiarity, alias.familiarity )
498
+ canon.resolutionConfidence = Math.max( canon.resolutionConfidence, alias.resolutionConfidence )
499
+ canon.lastSeenTick = Math.max( canon.lastSeenTick as unknown as number, alias.lastSeenTick as unknown as number ) as unknown as Tick
500
+ canon.valence = ( canon.valence + alias.valence ) / 2
501
+ canon.reliability = ( canon.reliability + alias.reliability ) / 2
502
+ canon.name ??= alias.name
503
+ for( const h of alias.handles ) canon.handles = withHandle( canon.handles, h )
504
+
505
+ // The doubt is answered — on both sides, and about each other only. A
506
+ // suspicion left standing after the fusion would have the mind keep wondering
507
+ // about a question it has already settled.
508
+ const settled = ( held: string[] | undefined ): string[] | undefined => {
509
+ const out = ( held ?? [] ).filter( k => k !== alias.keid && k !== canon.keid )
510
+ return out.length > 0 ? out : undefined
511
+ }
512
+ canon.suspectedSameAs = settled([ ...( canon.suspectedSameAs ?? [] ), ...( alias.suspectedSameAs ?? [] ) ])
513
+
514
+ // Every address that pointed at the absorbed referent must now point here, or
515
+ // the next message from one of them mints the person all over again.
516
+ for( const [ a, c ] of this._aliases )
517
+ if( c === alias.keid ) this._aliases.set( a, canon.keid )
518
+
519
+ this._dossiers.delete( alias.keid )
520
+ this._aliases.set( alias.keid, canon.keid )
521
+ commands.delete!.push(`ke-${ alias.keid }`)
522
+ commands.set!.push({ id: `kea-${ alias.keid }`, type: 'known-entity-alias',
523
+ metadata: { aliasKeid: alias.keid, canonicalKeid: canon.keid } })
524
+ }
525
+
336
526
  /** Resolution confidence: a learned name plus repeated encounters identify a referent. */
337
527
  private _resolution( d: KnownEntity ): number {
338
528
  return Math.min( 1, ( d.name ? 0.4 : 0 ) + Math.min( 0.6, d.encounterCount * 0.05 ) )
@@ -361,34 +551,61 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
361
551
  // handle is already an established relationship, or if both were active concurrently
362
552
  // (two interlocutors at once ⇒ distinct). A same-name match alone is too weak.
363
553
  const gap = Math.abs( ( canon.lastSeenTick as unknown as number ) - ( alias.lastSeenTick as unknown as number ) )
364
- if( alias.encounterCount >= RECOGNITION_MERGE_MAX_ENCOUNTERS || gap < RECOGNITION_CONCURRENCY_WINDOW )
554
+
555
+ // Concurrency is evidence AGAINST: two people talking at once are two
556
+ // people, and there is nothing to wonder about. Say nothing.
557
+ if( gap < RECOGNITION_CONCURRENCY_WINDOW ) continue
558
+
559
+ // Establishment is merely insufficient evidence FOR. The two may well be
560
+ // one someone; the heuristic is just not entitled to decide it, because
561
+ // absorbing an established relationship would take a real person's
562
+ // history with them. So the mind is told, and gets to settle it itself —
563
+ // usually the way anyone would, by asking.
564
+ if( alias.encounterCount >= RECOGNITION_MERGE_MAX_ENCOUNTERS ){
565
+ for( const [ a, b ] of [ [ canon, alias ], [ alias, canon ] ] as const ){
566
+ const held = a.suspectedSameAs ?? []
567
+ if( !held.includes( b.keid ) ) a.suspectedSameAs = [ ...held, b.keid ].sort()
568
+ }
569
+ merged = true // the doubt is a change worth persisting
365
570
  continue
571
+ }
366
572
 
367
- canon.encounterCount += alias.encounterCount
368
- canon.familiarity = Math.max( canon.familiarity, alias.familiarity )
369
- canon.resolutionConfidence = Math.max( canon.resolutionConfidence, alias.resolutionConfidence )
370
- canon.lastSeenTick = Math.max( canon.lastSeenTick as unknown as number, alias.lastSeenTick as unknown as number ) as unknown as Tick
371
- canon.valence = ( canon.valence + alias.valence ) / 2
372
- canon.reliability = ( canon.reliability + alias.reliability ) / 2
373
-
374
- this._dossiers.delete( alias.keid )
375
- this._aliases.set( alias.keid, canon.keid )
376
- commands.delete!.push(`ke-${alias.keid}`)
377
- commands.set!.push({ id: `kea-${alias.keid}`, type: 'known-entity-alias',
378
- metadata: { aliasKeid: alias.keid, canonicalKeid: canon.keid } })
573
+ this._fuse( canon, alias, commands )
379
574
  merged = true
380
575
  }
381
576
  }
382
577
  return merged
383
578
  }
384
579
 
580
+ /**
581
+ * The dossier for a referent, minting its anchor the first time it is met.
582
+ *
583
+ * A transport address arriving here (`discord:1019…`) is not an identity, it is
584
+ * a way the world happened to name someone. So it becomes an ALIAS of a fresh
585
+ * `ke:` anchor, and the dossier lives under the anchor. Everything downstream —
586
+ * reputation, theory-of-mind, attachment, goals, the PMA — keeps working
587
+ * untouched: it still sees one opaque string per referent, which is simply no
588
+ * longer a route. The same human met later on another channel resolves to this
589
+ * same anchor instead of becoming a second person nobody could connect.
590
+ */
385
591
  private _getOrCreate( keid: string, domain: string, tick: Tick ): KnownEntity {
386
- keid = this._aliases.get( keid ) ?? keid // a recognised alias lands on the canonical dossier
592
+ keid = canonicalOf( this._aliases, keid ) // an alias lands on the canonical dossier
387
593
  const existing = this._dossiers.get( keid )
388
594
  if( existing ) return existing
389
595
 
596
+ // Mint the anchor for a transport address seen for the first time. Deterministic
597
+ // from that address, so a replay of a recorded run mints the same id (R2).
598
+ let anchor = keid
599
+ if( !isReferentId( keid ) ){
600
+ anchor = mintReferentId( keid )
601
+ this._aliases.set( keid, anchor )
602
+ this._mintedAliases.push({ alias: keid, canonical: anchor })
603
+ const already = this._dossiers.get( anchor )
604
+ if( already ) return already
605
+ }
606
+
390
607
  const d: KnownEntity = {
391
- keid,
608
+ keid: anchor,
392
609
  kind: SENTIENT_DOMAINS.has( domain ) ? 'sentient' : 'thing',
393
610
  familiarity: 0,
394
611
  valence: 0,
@@ -396,15 +613,35 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
396
613
  encounterCount: 0,
397
614
  lastSeenTick: tick,
398
615
  resolutionConfidence: 0,
616
+ handles: [],
399
617
  }
400
- this._dossiers.set( keid, d )
618
+ this._dossiers.set( anchor, d )
401
619
  return d
402
620
  }
403
621
 
404
622
  /** Keep the most-familiar dossiers; absence-faded acquaintances fall away (forgetting). */
623
+ /**
624
+ * Forget the least-held referents when over capacity.
625
+ *
626
+ * Ranked by more than exposure, deliberately. This sorted on `familiarity`
627
+ * alone, which is MERE EXPOSURE — and now that a referent need not be a person
628
+ * (a document, a repo, a room), things get far more exposure than people do. A
629
+ * mind that touched sixty files would have evicted a colleague it speaks to
630
+ * weekly in favour of a config file it opened a lot, silently, taking that
631
+ * person's reputation, theory-of-mind model and attachment bond with it.
632
+ *
633
+ * So a referent the mind has actually got to know is stickier than one it has
634
+ * merely seen often: knowing their NAME is the single strongest signal (it is
635
+ * what distinguishes a someone from a blip), then how resolved the referent is,
636
+ * then exposure. Nothing here is about being a person — a named, well-resolved
637
+ * document outranks a glimpsed stranger, which is correct.
638
+ */
405
639
  private _prune(): void {
406
640
  if( this._dossiers.size <= this._maxTracked ) return
407
- const sorted = [ ...this._dossiers.values() ].sort( ( a, b ) => b.familiarity - a.familiarity )
641
+ const hold = ( d: KnownEntity ): number =>
642
+ ( d.name ? 1 : 0 ) + d.resolutionConfidence + d.familiarity
643
+ const sorted = [ ...this._dossiers.values() ]
644
+ .sort( ( a, b ) => hold( b ) - hold( a ) || ( a.keid < b.keid ? -1 : 1 ) )
408
645
  for( const d of sorted.slice( this._maxTracked ) ) this._dossiers.delete( d.keid )
409
646
  }
410
647
 
@@ -430,6 +667,8 @@ export class KnownEntityTracker implements SimulationEngine, CognitiveEngine {
430
667
  familiarity: ( m['familiarity'] as number ) ?? 0,
431
668
  valence: ( m['valence'] as number ) ?? 0,
432
669
  reliability: ( m['reliability'] as number ) ?? 0.5,
670
+ handles: Array.isArray( m['handles'] ) ? ( m['handles'] as Handle[] ) : [],
671
+ ...( Array.isArray( m['suspectedSameAs'] ) ? { suspectedSameAs: m['suspectedSameAs'] as string[] } : {} ),
433
672
  encounterCount: ( m['encounterCount'] as number ) ?? 0,
434
673
  lastSeenTick: ( m['lastSeenTick'] as Tick ) ?? ( 0 as unknown as Tick ),
435
674
  resolutionConfidence: ( m['resolutionConfidence'] as number ) ?? 0,
@@ -308,8 +308,13 @@ export class MoralEvaluator implements SimulationEngine, CognitiveEngine {
308
308
  })
309
309
  }
310
310
 
311
- // Own actions that could be violations
312
- if( entity.type === 'action.own' || entity.type === 'decision.record'){
311
+ // Own actions that could be violations. `decision.record` is what carries
312
+ // them (InhibitionController writes it); an `action.own` alternative was
313
+ // scanned alongside it and never written by anything (#114). Unlike the
314
+ // social/threat host seams, this is an INTERNAL record of the mind's own
315
+ // conduct — not something a host supplies — so the dead half is removed
316
+ // rather than documented.
317
+ if( entity.type === 'decision.record'){
313
318
  const
314
319
  outcome = entity.metadata?.outcome as string ?? '',
315
320
  harmful = entity.metadata?.harmful === true,
@@ -386,7 +391,7 @@ export class MoralEvaluator implements SimulationEngine, CognitiveEngine {
386
391
  let actionCount = 0
387
392
 
388
393
  for( const entity of state.entities.values() ){
389
- if( entity.type !== 'action.own' && entity.type !== 'decision.record') continue
394
+ if( entity.type !== 'decision.record') continue
390
395
 
391
396
  const
392
397
  virtuous = entity.metadata?.virtuous === true,
@@ -135,6 +135,39 @@ const CONSCIENTIOUSNESS_MORAL_GAIN = -0.2 // −moral.eventThreshold per unit
135
135
  // native scales — see the selector's effectiveSwitchCost. Lower gain to match the smaller base.
136
136
  const CONSCIENTIOUSNESS_SELECTOR_FOCUS_GAIN = 0.1 // +action-selector.switchCost per unit conscientiousness (resists action-interruption)
137
137
 
138
+ // Attentional BREADTH — how many focused facets this mind holds at once
139
+ // (engine-config-executive.maxFacets, the FacetSupervisor's ceiling). Two opposing
140
+ // dispositional pulls on one param, the pattern rules 17/17b already use: an open
141
+ // mind spreads itself across more at once, a conscientious one narrows to fewer and
142
+ // sees them through. Scaled against a base of 10, so a strongly-expressed trait moves
143
+ // the ceiling by a few threads, not by an order of magnitude — and consolidatePrior's
144
+ // per-step and cumulative caps bound it regardless.
145
+ const OPENNESS_BREADTH_GAIN = 4 // +executive.maxFacets per unit openness (hold more at once)
146
+ const CONSCIENTIOUSNESS_BREADTH_GAIN = -3 // −executive.maxFacets per unit conscientiousness (fewer, seen through)
147
+
148
+ // The same breadth disposition at the ATTENDING level — how many things the mind
149
+ // holds in view at once (engine-config-attention.maxFoci, the allocator's slot cap),
150
+ // as opposed to how many threads it keeps open (maxFacets above). One disposition,
151
+ // two owners — rules 28/28b's pattern. Scaled against a base of 4, so the same trait
152
+ // moves both levels proportionally rather than pulling them apart.
153
+ const OPENNESS_FOCI_GAIN = 1.5 // +attention.maxFoci per unit openness
154
+ const CONSCIENTIOUSNESS_FOCI_GAIN = -1.2 // −attention.maxFoci per unit conscientiousness
155
+
156
+ // How long the Will sits with something it has already said before saying it again
157
+ // (action-selector.repeatDamping, EXAFFERENCE P5). Agreeableness gives people room;
158
+ // demonstrated persistence chases the answer. Scaled against a base of 0.30.
159
+ const AGREEABLENESS_PATIENCE_GAIN = 0.12 // +repeatDamping per unit agreeableness
160
+ const PERSISTENCE_FOLLOWUP_GAIN = -0.10 // −repeatDamping per unit persistence
161
+ // The same disposition on the WINDOW rather than the strength: how long what it
162
+ // said stands before coming back to it. Scaled against a base of 60 ticks.
163
+ const AGREEABLENESS_WINDOW_GAIN = 24 // +repeatWindowTicks per unit agreeableness
164
+ const PERSISTENCE_WINDOW_GAIN = -20 // −repeatWindowTicks per unit persistence
165
+ // Who the mind is drawn toward. Agreeableness leans into reciprocity (toward the
166
+ // people who answer); demonstrated persistence leans the other way, and can carry
167
+ // this SIGNED weight negative — a mind that reaches hardest for the silence.
168
+ const AGREEABLENESS_RECIPROCITY_GAIN = 0.12 // +action-selector.socialWeight per unit agreeableness
169
+ const PERSISTENCE_RECIPROCITY_GAIN = -0.14 // −action-selector.socialWeight per unit persistence
170
+
138
171
  // Emotional stability (formed from observed affect dynamics, not task success) develops
139
172
  // the affect *reactivity gain* down: a steadier Will lets frustration snowball into
140
173
  // chronic irritability more slowly. Distinct axis from resilience — resilience tunes how
@@ -346,6 +379,11 @@ export class PersonaConsolidator implements SimulationEngine, CognitiveEngine {
346
379
  [ 'persona.executive.impulsivity_delta', next.priors[ 'engine-config-executive' ]?.impulsivity ?? 0 ],
347
380
  [ 'persona.frustration.irritability_rate_delta', next.priors[ 'engine-config-frustration' ]?.irritabilityRate ?? 0 ],
348
381
  [ 'persona.executive.deliberate_threshold_delta', next.priors[ 'engine-config-executive' ]?.deliberateThreshold ?? 0 ],
382
+ [ 'persona.executive.max_facets_delta', next.priors[ 'engine-config-executive' ]?.maxFacets ?? 0 ],
383
+ [ 'persona.attention.max_foci_delta', next.priors[ 'engine-config-attention' ]?.maxFoci ?? 0 ],
384
+ [ 'persona.selector.repeat_damping_delta', next.priors[ 'engine-config-action-selector' ]?.repeatDamping ?? 0 ],
385
+ [ 'persona.selector.repeat_window_delta', next.priors[ 'engine-config-action-selector' ]?.repeatWindowTicks ?? 0 ],
386
+ [ 'persona.selector.social_weight_delta', next.priors[ 'engine-config-action-selector' ]?.socialWeight ?? 0 ],
349
387
  [ 'persona.reward.social_weight_delta', next.priors[ 'engine-config-reward' ]?.socialWeight ?? 0 ],
350
388
  [ 'persona.frustration.anger_reactivity_delta', next.priors[ 'engine-config-frustration' ]?.angerReactivity ?? 0 ],
351
389
  [ 'persona.novelty.significance_threshold_delta', next.priors[ 'engine-config-novelty' ]?.significanceThreshold ?? 0 ],
@@ -730,6 +768,109 @@ export class PersonaConsolidator implements SimulationEngine, CognitiveEngine {
730
768
  engineConfigId: 'engine-config-task-switcher',
731
769
  param: 'baseSwitchCost'
732
770
  },
771
+ // 27c. Attentional BREADTH — how many things this mind holds at once (the
772
+ // FacetSupervisor's facet ceiling). Openness widens it; conscientiousness
773
+ // narrows it. Two opposing pulls on one param (rules 17/17b's pattern), so a
774
+ // Will that is both lands somewhere of its own rather than at either extreme.
775
+ // This is what makes "I can hold five conversations" or "I do one thing at a
776
+ // time" a fact about the person instead of a constant — the ceiling is the
777
+ // persona's; spare attention only scales the live allowance within it.
778
+ {
779
+ magnitude: openDev,
780
+ threshold: GRIT_THRESHOLD,
781
+ gain: OPENNESS_BREADTH_GAIN,
782
+ engineConfigId: 'engine-config-executive',
783
+ param: 'maxFacets'
784
+ },
785
+ {
786
+ magnitude: conscDev,
787
+ threshold: GRIT_THRESHOLD,
788
+ gain: CONSCIENTIOUSNESS_BREADTH_GAIN,
789
+ engineConfigId: 'engine-config-executive',
790
+ param: 'maxFacets'
791
+ },
792
+ // 27d. The same breadth disposition one level down: how many things the mind
793
+ // holds IN VIEW at once (the allocator's `maxFoci` slots), as against how
794
+ // many threads it keeps OPEN (27c). A reasoning facet now competes for
795
+ // these slots like any percept, so the two levels are one economy — a Will
796
+ // can be in ten conversations while attending to two, and which two is
797
+ // settled by salience against everything else it could be noticing.
798
+ // `maxFoci` was already config-mirrored and read through the persona-prior;
799
+ // it just had no description and no edge, so it could never actually move.
800
+ {
801
+ magnitude: openDev,
802
+ threshold: GRIT_THRESHOLD,
803
+ gain: OPENNESS_FOCI_GAIN,
804
+ engineConfigId: 'engine-config-attention',
805
+ param: 'maxFoci'
806
+ },
807
+ {
808
+ magnitude: conscDev,
809
+ threshold: GRIT_THRESHOLD,
810
+ gain: CONSCIENTIOUSNESS_FOCI_GAIN,
811
+ engineConfigId: 'engine-config-attention',
812
+ param: 'maxFoci'
813
+ },
814
+ // 27e. How long the Will lets its own words stand before repeating them. A
815
+ // delivered act leaves a live footprint (EXAFFERENCE P5) that damps redoing
816
+ // it; how HARD it damps is a disposition. An agreeable Will gives people
817
+ // room after saying its piece; a persistent one chases the answer sooner.
818
+ // Opposing pulls on one param, same as 17/17b. This is the trait behind
819
+ // "she messaged me the same thing three times" — the mechanism stops the
820
+ // loop, this decides where in the range between patient and dogged she sits.
821
+ {
822
+ magnitude: agreeableDev,
823
+ threshold: GRIT_THRESHOLD,
824
+ gain: AGREEABLENESS_PATIENCE_GAIN,
825
+ engineConfigId: 'engine-config-action-selector',
826
+ param: 'repeatDamping'
827
+ },
828
+ {
829
+ magnitude: persistDev,
830
+ threshold: GRIT_THRESHOLD,
831
+ gain: PERSISTENCE_FOLLOWUP_GAIN,
832
+ engineConfigId: 'engine-config-action-selector',
833
+ param: 'repeatDamping'
834
+ },
835
+ // 27f. The same patience on the satiation WINDOW rather than its strength —
836
+ // 27e decides how hard having spoken damps speaking again, this decides
837
+ // for how long. Kept separate because a mind can be quick to repeat but
838
+ // only briefly, or slow to repeat but for a long time.
839
+ {
840
+ magnitude: agreeableDev,
841
+ threshold: GRIT_THRESHOLD,
842
+ gain: AGREEABLENESS_WINDOW_GAIN,
843
+ engineConfigId: 'engine-config-action-selector',
844
+ param: 'repeatWindowTicks'
845
+ },
846
+ {
847
+ magnitude: persistDev,
848
+ threshold: GRIT_THRESHOLD,
849
+ gain: PERSISTENCE_WINDOW_GAIN,
850
+ engineConfigId: 'engine-config-action-selector',
851
+ param: 'repeatWindowTicks'
852
+ },
853
+ // 27g. Who the Will is drawn toward. `socialWeight` scales its learned read on
854
+ // a person in the action competition, and it is the one weight left
855
+ // deliberately SIGNED: agreeableness leans into reciprocity (toward the
856
+ // people who answer), demonstrated persistence leans against it and can
857
+ // carry the weight negative — a mind that reaches hardest for the silence
858
+ // precisely because it is silent. Both are coherent colleagues, so the
859
+ // container declines to pick and lets the persona land where it lands.
860
+ {
861
+ magnitude: agreeableDev,
862
+ threshold: GRIT_THRESHOLD,
863
+ gain: AGREEABLENESS_RECIPROCITY_GAIN,
864
+ engineConfigId: 'engine-config-action-selector',
865
+ param: 'socialWeight'
866
+ },
867
+ {
868
+ magnitude: persistDev,
869
+ threshold: GRIT_THRESHOLD,
870
+ gain: PERSISTENCE_RECIPROCITY_GAIN,
871
+ engineConfigId: 'engine-config-action-selector',
872
+ param: 'socialWeight'
873
+ },
733
874
  // 28b. Same disposition, the AGENCY selector's owner (R2): a conscientious Will also
734
875
  // resists having an in-flight action preempted — raise the selector's switch-cost
735
876
  // hysteresis. Shares the conscientiousness driver with rule 28 (one switch-resistance