@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
@@ -0,0 +1,260 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/agency/conversation.aim.ts — did the words achieve what they were for?
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // A communicative act has an AIM that is not its EXECUTION. Sending succeeded;
6
+ // that is a fact about the channel. Whether anyone answered is a fact about the
7
+ // world, it arrives later, and until this module existed nothing in the mind
8
+ // represented it.
9
+ //
10
+ // The cost of not representing it, measured on a live Will: `agency.skill` for
11
+ // `reach-out` read `enactments 28, successes 28` — a 100% success rate, because
12
+ // the outbox accepted every one. Habit (0.78) and value (0.5) therefore rose with
13
+ // each repetition while the only opposing term, satiation, is bounded and decays.
14
+ // The mind asked one person the same question eleven times in two and a half
15
+ // minutes and, from the inside, every one of them was the first: it could see
16
+ // `✓ reach-out` in its recent outcomes and nothing at all about being ignored.
17
+ //
18
+ // So this is not a rule against repeating. It is the missing PERCEPT. The mind
19
+ // gets to know that it spoke and was not answered; what to do about that is its
20
+ // own business — a person who has been ignored twice may well try a third time,
21
+ // and should be able to, having noticed.
22
+ //
23
+ // Everything here is pure and tick-denominated: no wall clock, no RNG, no state
24
+ // writes. Ids and decisions stay identical between a recorded run and its replay
25
+ // (R2). The resolution is folded into state by ReafferenceEngine, which already
26
+ // owns the question "did the world confirm my act?".
27
+ // ─────────────────────────────────────────────────────────────
28
+
29
+ import type { Tick } from '#core/types'
30
+ import { readAliases, canonicalOf } from '#cognition/social.identity'
31
+
32
+ export const SENT_TYPE = 'conversation.sent'
33
+ export const RECEIVED_TYPE = 'conversation.received'
34
+
35
+ /**
36
+ * How long the mind waits before it counts a silence as an answer in itself.
37
+ *
38
+ * Deliberately much longer than the echo window (`CONSEQUENCE_TTL_TICKS`, 30), a
39
+ * motor await (`AWAIT_TIMEOUT`, 15), or satiation (`repeatWindowTicks`, 60).
40
+ * Those ask "did my act leave the building?" and "does saying it again feel right
41
+ * yet?"; this asks "did a person get back to me?", and people take their time. At
42
+ * the 1s tick a hosted Will actually runs on, 240 is four minutes — long enough
43
+ * that a silence is genuinely a silence and not somebody typing.
44
+ *
45
+ * Tuned through the persona prior rather than fixed, because how long a quiet
46
+ * takes to mean something is a trait — the same reason `facetIdleTtlTicks` and
47
+ * `repeatWindowTicks` are not constants.
48
+ */
49
+ export const DEFAULT_REPLY_WINDOW_TICKS = 240
50
+
51
+ /** Minimal shape this module needs off a state entity — keeps it testable without a StateManager. */
52
+ export interface EntityLike {
53
+ type: string
54
+ /**
55
+ * The real field. `StateManager.setEntity` stamps it from the sim clock on
56
+ * every write, and it is the ONLY tick a record written off-tick can have —
57
+ * an inbound turn arrives between ticks and its writer has no clock to quote.
58
+ */
59
+ updatedAtTick?: number
60
+ /** Not on `SimulationEntity`; accepted so a test can state a tick directly. */
61
+ tick?: number
62
+ metadata?: ReadonlyMap<string, unknown> | Record<string, unknown>
63
+ }
64
+
65
+ /**
66
+ * When a record happened: what its writer declared, else when the state manager
67
+ * stamped it.
68
+ *
69
+ * `SimulationEntity` has no `tick` — it is `updatedAtTick`, and reading the
70
+ * former silently yields `undefined` on every real entity. That is not a
71
+ * hypothetical: this module shipped reading `e.tick`, so `lastHeard` was 0 for
72
+ * everybody, `0 > sentTick` was never true, and NO turn could ever be marked
73
+ * answered. Caught on a live run where she was mid-conversation and the engine
74
+ * announced "no answer from Fabrice" about a message he had already replied to.
75
+ *
76
+ * The same dead read sits in `spokenAtByEntity` (agency/consequence.ts) behind a
77
+ * comment explaining why the fallback matters. It never fired there either.
78
+ */
79
+ export function tickOf( e: EntityLike, meta: Record<string, unknown> ): Tick {
80
+ return ( num( meta['tick'] ) ?? num( e.updatedAtTick ) ?? num( e.tick ) ?? 0 ) as Tick
81
+ }
82
+
83
+ /** One thing the mind said to one person, and what became of it. */
84
+ export interface SpokenTurn {
85
+ entityId: string
86
+ targetEntityId: string
87
+ targetEntityName?: string
88
+ preview: string
89
+ tick: Tick
90
+ /** Set once the target has spoken after this turn. */
91
+ answeredAt?: Tick
92
+ /**
93
+ * WHAT they said back.
94
+ *
95
+ * Recorded on the turn rather than looked up later, because a
96
+ * `conversation.received` lives exactly one tick — SocialPerception sweeps it as
97
+ * a one-shot event — so the text is unavailable by the next render. Without it
98
+ * the mind is told an answer ARRIVED and never shown it, which is worse than
99
+ * silence: it invites the mind to act as though it has the answer. Live, that
100
+ * put a wrong meeting time in front of a third party — she asked "same time,
101
+ * 3pm?", was told `they answered`, never saw the correction to 2pm, and relayed
102
+ * 3pm as confirmed.
103
+ */
104
+ answeredWith?: string
105
+ /** Set once the reply window closed on a silence — recorded so it is announced once, not every tick. */
106
+ unansweredAt?: Tick
107
+ /** An acknowledgement closes a turn rather than opening one; it awaits nothing. */
108
+ isAck: boolean
109
+ }
110
+
111
+ function meta( e: EntityLike ): Record<string, unknown> {
112
+ const m = e.metadata
113
+ if( !m ) return {}
114
+ return m instanceof Map ? Object.fromEntries( m ) : m as Record<string, unknown>
115
+ }
116
+ function str( v: unknown ): string | undefined { return typeof v === 'string' ? v : undefined }
117
+ function num( v: unknown ): number | undefined {
118
+ return typeof v === 'number' && Number.isFinite( v ) ? v : undefined
119
+ }
120
+
121
+ /**
122
+ * Every `conversation.sent` in state, as turns.
123
+ *
124
+ * The tick comes from `metadata.tick` where the writer set one, else the entity's
125
+ * own tick — `StateManager.setEntity` stamps that from the sim clock on every
126
+ * write. The fallback is not decoration: the two writers (ProactiveCommunicator
127
+ * and AuditionEngine) disagree about which they fill, and defaulting to 0 made
128
+ * every record of the second kind look infinitely old. Same rule as
129
+ * `spokenAtByEntity`, deliberately.
130
+ */
131
+ export function readSpokenTurns( entities: ReadonlyMap<string, EntityLike> ): SpokenTurn[] {
132
+ // Through the alias table, because the two writers name their target in
133
+ // different id spaces: a REPLY is addressed to the transport id the percept
134
+ // arrived with (`discord:1019…`), while a PROACTIVE message is addressed to the
135
+ // anchor the executive resolved (`ke:…`). Both are the same someone. Matched
136
+ // raw, an answer to one could never close the other — the same two-halves-in-
137
+ // two-id-spaces failure that made `answered` impossible the first time.
138
+ const aliases = readAliases( entities )
139
+ const out: SpokenTurn[] = []
140
+ for( const [ id, e ] of entities ){
141
+ if( e.type !== SENT_TYPE ) continue
142
+ const m = meta( e )
143
+ const target = str( m['targetEntityId'] )
144
+ if( !target ) continue
145
+ out.push({
146
+ entityId: id,
147
+ targetEntityId: canonicalOf( aliases, target ),
148
+ targetEntityName: str( m['targetEntityName'] ),
149
+ preview: str( m['preview'] ) ?? '',
150
+ tick: tickOf( e, m ),
151
+ answeredAt: num( m['answeredAt'] ) as Tick | undefined,
152
+ answeredWith: str( m['answeredWith'] ),
153
+ unansweredAt: num( m['unansweredAt'] ) as Tick | undefined,
154
+ isAck: m['isAck'] === true,
155
+ })
156
+ }
157
+ // Stable, deterministic order: oldest first, ties broken by id so two turns on
158
+ // one tick never swap between a run and its replay.
159
+ return out.sort( ( a, b ) => a.tick - b.tick || ( a.entityId < b.entityId ? -1 : a.entityId > b.entityId ? 1 : 0 ) )
160
+ }
161
+
162
+ /**
163
+ * When each person last spoke TO the mind, from `conversation.received`.
164
+ *
165
+ * This is the evidence that an utterance was answered. It is deliberately the
166
+ * whole of the evidence: any turn from them after we spoke counts, without trying
167
+ * to decide whether it was *about* what we said. Matching on topic would be a
168
+ * guess dressed as a fact, and getting it wrong in the strict direction would
169
+ * teach the mind it is being ignored by someone who is talking to it.
170
+ */
171
+ export function lastHeardByEntity( entities: ReadonlyMap<string, EntityLike> ): Map<string, Heard> {
172
+ const aliases = readAliases( entities )
173
+ const out = new Map<string, Heard>()
174
+ for( const [ , e ] of entities ){
175
+ if( e.type !== RECEIVED_TYPE ) continue
176
+ const m = meta( e )
177
+ const raw = str( m['sourceKeid'] )
178
+ if( !raw ) continue
179
+ const source = canonicalOf( aliases, raw )
180
+ const at = tickOf( e, m )
181
+ if( at > ( out.get( source )?.tick ?? -Infinity ) )
182
+ out.set( source, { tick: at, preview: str( m['preview'] ) ?? '' } )
183
+ }
184
+ return out
185
+ }
186
+
187
+ /** When someone last spoke to us, AND what they said. */
188
+ export interface Heard { tick: Tick; preview: string }
189
+
190
+ /** A turn still in the air: said, not acknowledged-only, and not yet answered. */
191
+ export function isOpen( t: SpokenTurn ): boolean {
192
+ return !t.isAck && t.answeredAt === undefined
193
+ }
194
+
195
+ export interface Resolution {
196
+ /** Turns the target has now answered — fold `answeredAt` + what they said into them. */
197
+ answered: Array<{ turn: SpokenTurn; at: Tick; with: string }>
198
+ /** Turns whose reply window closed in silence, newly, this tick. */
199
+ unanswered: SpokenTurn[]
200
+ }
201
+
202
+ /**
203
+ * Fold the world's answer (or its absence) onto the turns still in the air.
204
+ *
205
+ * Pure: reads frozen state, writes nothing, decides nothing about what the mind
206
+ * should do next. A turn resolves exactly once — `answeredAt`/`unansweredAt` are
207
+ * the latches, so the announcement fires on one tick rather than every tick for
208
+ * the rest of the session.
209
+ *
210
+ * A silence is only reported once the window has fully elapsed. Before that the
211
+ * turn is simply open, which is a third state and the honest one: not yet
212
+ * answered is not the same as ignored, and collapsing them would have the mind
213
+ * conclude it was being snubbed one tick after speaking.
214
+ */
215
+ export function resolveReplyExpectations(
216
+ entities: ReadonlyMap<string, EntityLike>,
217
+ tick: Tick,
218
+ windowTicks: number = DEFAULT_REPLY_WINDOW_TICKS,
219
+ ): Resolution {
220
+ const turns = readSpokenTurns( entities )
221
+ const lastHeard = lastHeardByEntity( entities )
222
+
223
+ const answered: Resolution['answered'] = []
224
+ const unanswered: SpokenTurn[] = []
225
+
226
+ for( const t of turns ){
227
+ if( t.isAck || t.answeredAt !== undefined ) continue
228
+
229
+ const heard = lastHeard.get( t.targetEntityId )
230
+ if( heard !== undefined && heard.tick > t.tick ){
231
+ answered.push({ turn: t, at: heard.tick, with: heard.preview })
232
+ continue
233
+ }
234
+
235
+ // Already announced as a silence — stays open, but says nothing more.
236
+ if( t.unansweredAt !== undefined ) continue
237
+ // A turn stamped LATER than now came back with a restored snapshot whose tick
238
+ // counter has not caught up. Reading it as `tick - t.tick` negative would make
239
+ // it trivially "not yet due" forever, which is the benign direction, but a
240
+ // window <= 0 would then fire on everything at once. Guard both.
241
+ if( windowTicks > 0 && tick - t.tick >= windowTicks ) unanswered.push( t )
242
+ }
243
+
244
+ return { answered, unanswered }
245
+ }
246
+
247
+ /**
248
+ * The turns worth showing the mind: still in the air, newest first, capped.
249
+ *
250
+ * This is what makes repetition visible from the inside. `conversation.sent` has
251
+ * always been in state — 57 records on the Will above — and reached no prompt at
252
+ * all, so the mind's only view of having spoken was `✓ reach-out` under "Recent
253
+ * Action Outcomes": a tick mark, no words, no person, no silence.
254
+ */
255
+ export function openTurns(
256
+ entities: ReadonlyMap<string, EntityLike>,
257
+ limit: number = 6,
258
+ ): SpokenTurn[] {
259
+ return readSpokenTurns( entities ).filter( isOpen ).reverse().slice( 0, limit )
260
+ }
@@ -38,6 +38,7 @@ import {
38
38
  import { readEffectiveParams, readPersonaPrior } from '#cognition/persona.prior'
39
39
  import { RUPTURE_REVOKE_GATE, revocationEntity } from '#agency/revocation'
40
40
  import { liveConsequences, matchConsequenceText } from '#agency/consequence'
41
+ import { asFinality } from '#stem/policy/arbiter'
41
42
 
42
43
  /**
43
44
  * Activation margin (winner − runner-up) below which the choice is "contested" —
@@ -370,6 +371,42 @@ export class ActionSelector implements CognitiveEngine {
370
371
  const winner = scored[0]
371
372
  if( !winner ) return busy( eligible.length )
372
373
 
374
+ // Whenever more than one person is a candidate for contact, record how the
375
+ // competition between them actually resolved. A Will that names someone and
376
+ // never messages them looks identical from outside whether the intention was
377
+ // never formed or was formed and beaten every cycle; this is the only place
378
+ // that difference is visible, and it is one line per contested tick.
379
+ // Why a WILLED contact did not happen. Gated on the executive having actually
380
+ // willed one (source 'ideomotor') that is not the winner — so a mind whose
381
+ // decision is being enacted logs nothing, and a mind that decided to contact
382
+ // someone and then did not says exactly what beat it.
383
+ //
384
+ // The earlier version of this gated on `reachers.length > 1`, assuming the
385
+ // failure was one addressee out-competing another. It is not: each master cycle
386
+ // sweeps every executive intent it did not name that cycle, so there is usually
387
+ // exactly ONE reach-out candidate in the field and the contest log never fired.
388
+ // The competition that matters is the willed contact against the rest of the
389
+ // field, which is what this prints.
390
+ // EVERY willed contact that lost, not just the strongest. An earlier version
391
+ // used `.find()`, which reports only the highest-scoring one — so with two
392
+ // live intents a second loser went unlogged, and the log would have quietly
393
+ // understated how many people the mind decided to contact and then did not.
394
+ const lostReaches = scored.filter( s =>
395
+ s !== winner
396
+ && s.affordance.schema === 'reach-out'
397
+ && s.affordance.source === 'ideomotor'
398
+ && s.affordance.targetEntityId )
399
+
400
+ for( const lost of lostReaches )
401
+ logger.info(
402
+ `[selector] willed reach-out → ${ lost.affordance.targetEntityId } NOT selected: ` +
403
+ `${ lost.activation.toFixed( 3 ) }` +
404
+ `${ lost.affordance.justEnacted ? ` (justEnacted ${ lost.affordance.justEnacted.toFixed( 2 ) })` : '' }` +
405
+ ` < ${ winner.affordance.schema }` +
406
+ `${ winner.affordance.targetEntityId ? `→${ winner.affordance.targetEntityId }` : '' }` +
407
+ ` ${ winner.activation.toFixed( 3 ) }`
408
+ )
409
+
373
410
  // ── Preempt a mid-composite routine (IMMEDIATE SWITCH) ────────
374
411
  // A strong/high-stakes challenger cuts the routine off AND takes the body
375
412
  // the same tick. We cannot delete the parent here: the executor runs later
@@ -425,12 +462,37 @@ export class ActionSelector implements CognitiveEngine {
425
462
  const sameAction = winner.affordance.schema === awaiting.schema && ( winner.affordance.targetEntityId ?? '') === awaiting.target
426
463
  if( sameAction ) return busy( eligible.length ) // field still favours what we await
427
464
 
428
- const staleness = Math.min( 1, ( tick - awaiting.dispatchedAt ) / AWAIT_STALE_TICKS )
465
+ // Clamped at BOTH ends. The upper bound was always there; the lower one matters
466
+ // because a restored intent's age is negative (state snapshots, the tick counter
467
+ // restarts at 1), and an unclamped negative staleness flips the decay into
468
+ // amplification: `1 - (-39 × 0.5)` = 20.6×, turning a 0.47 incumbent into 9.74 and
469
+ // making it permanently unpreemptable. The executor now clears such intents, so
470
+ // this is the second line of defence rather than the fix — but an incumbent's
471
+ // hysteresis must never be able to exceed its own recorded activation, whatever
472
+ // arithmetic feeds it.
473
+ const staleness = Math.min( 1, Math.max( 0, ( tick - awaiting.dispatchedAt ) / AWAIT_STALE_TICKS ) )
429
474
  const incumbentStrength = awaiting.activation * ( 1 - staleness * STALE_DECAY )
430
475
  const switchCost = effSwitchCost * ( 1 - stakes( winner.affordance, bias ) )
431
476
 
432
- if( winner.activation <= incumbentStrength + switchCost )
477
+ if( winner.activation <= incumbentStrength + switchCost ){
478
+ // A challenger aimed at a DIFFERENT person losing to an awaiting incumbent
479
+ // is the starvation shape: every proactive reach-out sits 'awaiting' while a
480
+ // facet authors its words (8–18s = many ticks), and the incumbent's recorded
481
+ // activation does not decay with the challenger's damping. If one addressee
482
+ // can hold the channel this way, a Will can decide to contact someone else
483
+ // every cycle and never once do it — which is exactly the report.
484
+ if( winner.affordance.schema === 'reach-out'
485
+ && winner.affordance.targetEntityId
486
+ && winner.affordance.targetEntityId !== awaiting.target )
487
+ logger.info(
488
+ `[selector] reach-out → ${ winner.affordance.targetEntityId } BLOCKED by awaiting ` +
489
+ `${ awaiting.schema } → ${ awaiting.target || '—' }: ` +
490
+ `challenger ${ winner.activation.toFixed( 3 ) } ≤ incumbent ${ incumbentStrength.toFixed( 3 ) } ` +
491
+ `+ switch ${ switchCost.toFixed( 3 ) } (awaiting ${ tick - awaiting.dispatchedAt } ticks)`
492
+ )
493
+
433
494
  return busy( eligible.length ) // not worth interrupting — keep waiting
495
+ }
434
496
 
435
497
  preemptDelete = awaiting.id // PREEMPT — fall through and commit the challenger
436
498
  preemptedFrom = awaiting.schema
@@ -477,6 +539,11 @@ export class ActionSelector implements CognitiveEngine {
477
539
  targetEntityId: winner.affordance.targetEntityId,
478
540
  parameters: winner.affordance.parameters,
479
541
  source: winner.affordance.source,
542
+ // What evoked this — for an ideomotor winner, the `ideomotor.intent` entity
543
+ // the executive wrote. Carried so the executor can DISCHARGE it once the act
544
+ // happens: an intention that has been acted on is no longer an intention, and
545
+ // nothing was deleting these. See MotorSchemaExecutor._dischargeWill.
546
+ ...( winner.affordance.evokedBy ? { evokedBy: winner.affordance.evokedBy } : {} ),
480
547
  // Plan provenance (when a plan's frontier-step prior won the competition) —
481
548
  // flows through the executor's action.outcome so the PlanningEngine advances.
482
549
  ...( winner.affordance.planId ? { planId: winner.affordance.planId } : {} ),
@@ -657,7 +724,7 @@ function refusedClassSchemas( state: ReadonlySimulationState ): Set<string> {
657
724
  for( const e of state.entities.values() ){
658
725
  if( e.type !== 'agency.outcome') continue
659
726
  const m = e.metadata
660
- if( m?.['refused'] !== true || str( m?.['finality'] ) !== 'class') continue
727
+ if( m?.['refused'] !== true || asFinality( m?.['finality'] ) !== 'class') continue
661
728
  const schema = str( m?.['schema'] )
662
729
  if( schema ) out.add( schema )
663
730
  }
@@ -676,6 +743,19 @@ function effectiveWeights( state: ReadonlySimulationState ): ScoreWeights {
676
743
  ...DEFAULT_WEIGHTS,
677
744
  risk: Math.max( 0, num( p['riskWeight'], DEFAULT_WEIGHTS.risk ) ),
678
745
  novelty: Math.max( 0, num( p['noveltyWeight'], DEFAULT_WEIGHTS.novelty ) ),
746
+ // How long the mind sits with something it has already said before saying it
747
+ // again. Agreeableness raises it (does not badger); demonstrated persistence
748
+ // lowers it (follows up sooner). Clamped at 0 so a prior can flatten the
749
+ // damping into indifference but never turn repeating into a *reward*.
750
+ repeat: Math.max( 0, num( p['repeatDamping'], DEFAULT_WEIGHTS.repeat ) ),
751
+ // How much the mind's learned read on a person biases acting toward them.
752
+ // Deliberately NOT clamped at 0 — this weight is genuinely signed territory,
753
+ // and it is the tenant's to occupy. A warm, reciprocal mind leans toward
754
+ // whoever answers; a dogged, duty-bound one chases a silence precisely
755
+ // because it is silent. Both are coherent people. The container supplies the
756
+ // term and the persona supplies the sign; hardcoding it here would be the
757
+ // container deciding what kind of colleague every tenant has to be.
758
+ social: num( p['socialWeight'], DEFAULT_WEIGHTS.social ),
679
759
  }
680
760
  }
681
761
 
@@ -728,6 +808,8 @@ function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<st
728
808
  available: meta['available'] === true,
729
809
  tags: Array.isArray( meta['tags'] ) ? ( meta['tags'] as unknown[] ).filter( ( t ): t is string => typeof t === 'string') : [],
730
810
  planBias: typeof meta['planBias'] === 'number' ? ( meta['planBias'] as number ) : undefined,
811
+ willBias: typeof meta['willBias'] === 'number' ? ( meta['willBias'] as number ) : undefined,
812
+ socialPrior: typeof meta['socialPrior'] === 'number' ? ( meta['socialPrior'] as number ) : undefined,
731
813
  planId: str( meta['planId'] ),
732
814
  stepId: str( meta['stepId'] ),
733
815
  tick: num( meta['tick'], 0 ),
@@ -36,6 +36,8 @@ import type { Affordance, AffordanceSource, MotorSchema, LearnedSkill, SchemaPre
36
36
  import type { SchemaRepertoire } from '#agency/schemas/repertoire'
37
37
  import { INNATE_SCHEMAS } from '#agency/schemas/innate'
38
38
  import { collectGoalTargets } from '#agency/selection.scoring'
39
+ import { readEffectiveParams } from '#cognition/persona.prior'
40
+ import { liveConsequences, enactionFootprint, spokenAtByEntity, CONSEQUENCE_TTL_TICKS, type ConsequenceDescriptor } from '#agency/consequence'
39
41
 
40
42
  /** Default field width for non-innate affordances when no attention metric exists. */
41
43
  const DEFAULT_ATTENTION_CAP = 5
@@ -59,6 +61,19 @@ export class AffordanceSynthesizer implements CognitiveEngine {
59
61
  private _schemas: MotorSchema[]
60
62
  private _skills: SkillAccessor | null = null
61
63
  private _repertoire: SchemaRepertoire | null = null
64
+
65
+ /**
66
+ * This tick's live consequence descriptors — the acts the mind has performed
67
+ * whose outcome has not yet come back. Refreshed once at the top of react()
68
+ * because `_build` runs per candidate and reading them is a full-entity scan.
69
+ */
70
+ private _inFlight: readonly ConsequenceDescriptor[] = []
71
+
72
+ /** Ticks an act stays satiating (engine-config-action-selector.repeatWindowTicks). */
73
+ private _satiationWindow: number = CONSEQUENCE_TTL_TICKS
74
+
75
+ /** Tick of the last thing said to each entity — outlives the descriptor sweep. */
76
+ private _spokenAt: ReadonlyMap<string, number> = new Map()
62
77
  private _bus: CognitiveBus | null = null
63
78
  private _defaultCap: number
64
79
  private _lastFieldSize = 0
@@ -112,6 +127,21 @@ export class AffordanceSynthesizer implements CognitiveEngine {
112
127
  const valence = metric( state, 'affect.valence', 0 )
113
128
  const energyLow = metric( state, 'energy.level', 0 ) < 30
114
129
 
130
+ // The mind's own acts still in flight (EXAFFERENCE P5). Read once per tick —
131
+ // `_build` runs for every candidate and this is a full-entity scan. Each
132
+ // candidate whose (schema, target) matches one of these carries a decaying
133
+ // `justEnacted`, so having just done a thing damps doing it again.
134
+ this._inFlight = liveConsequences( state.entities, tick )
135
+ // How long this mind sits with something it has already done before doing it
136
+ // again — the tenant's, not the container's. Falls back to the echo TTL only
137
+ // when unseeded, so a bare harness behaves as before.
138
+ this._satiationWindow = readEffectiveParams( state, 'engine-config-action-selector').repeatWindowTicks
139
+ ?? CONSEQUENCE_TTL_TICKS
140
+ // Durable "when did I last speak to them". Descriptors alone cannot carry
141
+ // satiation — the executor deletes them at their echo TTL, so any window
142
+ // longer than that was a no-op.
143
+ this._spokenAt = spokenAtByEntity( state.entities )
144
+
115
145
  const set: EntityInput[] = []
116
146
  const del: string[] = []
117
147
 
@@ -204,13 +234,18 @@ export class AffordanceSynthesizer implements CognitiveEngine {
204
234
 
205
235
  if( !schema ) continue
206
236
 
237
+ // `priority` is the confidence the executive decided with. It sets BOTH the
238
+ // evoke-salience (field admission) and — via willBias — the activation the
239
+ // selector scores, so a decision made at 0.85 pushes harder than one at 0.5.
240
+ const willBias = clamp01( num( m?.['priority'], 0.8 ) )
207
241
  candidates.push({
208
- salience: IDEOMOTOR_BASE_SALIENCE + num( m?.['priority'], 0.8 ),
242
+ salience: IDEOMOTOR_BASE_SALIENCE + willBias,
209
243
  affordance: this._build( schema, tick, state, valence, energyLow, skills, {
210
244
  evokedBy: id,
211
245
  targetEntityId: str( m?.['targetEntityId'] ),
212
246
  parameters: ( m?.['parameters'] as Record<string, unknown> ) ?? {},
213
247
  source: 'ideomotor',
248
+ willBias,
214
249
  } ),
215
250
  })
216
251
  }
@@ -303,6 +338,7 @@ export class AffordanceSynthesizer implements CognitiveEngine {
303
338
  parameters?: Record<string, unknown>
304
339
  source?: AffordanceSource
305
340
  planBias?: number
341
+ willBias?: number
306
342
  planId?: string
307
343
  stepId?: string
308
344
  },
@@ -313,6 +349,13 @@ export class AffordanceSynthesizer implements CognitiveEngine {
313
349
  // Will's affordance field is byte-identical. Present only once a refusal dented it.
314
350
  const availability = this._repertoire?.availabilityOf( schema.id ) ?? 1
315
351
 
352
+ // What the mind has LEARNED about the person this act is aimed at. Only for a
353
+ // targeted act; 0 for everything else, so the field is unchanged for a mind that
354
+ // knows no one. See Affordance.socialPrior.
355
+ const socialPrior = ctx.targetEntityId
356
+ ? socialStanding( state, ctx.targetEntityId )
357
+ : 0
358
+
316
359
  // Learned value if known, else the schema's intrinsic prior mapped to 0..1.
317
360
  const expectedReward = skill?.valueEstimate ?? clamp01( ( ( schema.baseValence ?? 0 ) + 1 ) / 2 )
318
361
  const expectedValence = schema.baseValence ?? valence
@@ -320,6 +363,16 @@ export class AffordanceSynthesizer implements CognitiveEngine {
320
363
  // Low energy makes everything feel costlier.
321
364
  const cost = clamp01( schema.cost * ( energyLow ? 1.5 : 1 ) )
322
365
 
366
+ // This act's own footprint, if it is still in flight toward this same person.
367
+ // Satiation only applies to acts aimed at someone the mind SPEAKS to — the
368
+ // `conversation.sent` half is keyed by person, so it must not damp, say,
369
+ // inspecting them.
370
+ const speaks = schema.tags?.includes('communication') ?? false
371
+ const justEnacted = enactionFootprint(
372
+ this._inFlight, schema.id, ctx.targetEntityId, tick, this._satiationWindow,
373
+ speaks ? this._spokenAt : undefined,
374
+ )
375
+
323
376
  const key = ctx.targetEntityId ?? ctx.evokedBy ?? schema.id
324
377
  const source = ctx.source ?? schema.source
325
378
  // A non-default-source candidate (ideomotor) gets a distinct id so it can coexist
@@ -341,7 +394,10 @@ export class AffordanceSynthesizer implements CognitiveEngine {
341
394
  tags: schema.tags ?? [],
342
395
  ...( schema.description ? { description: schema.description } : {} ),
343
396
  ...( availability < 1 ? { availability } : {} ),
397
+ ...( socialPrior !== 0 ? { socialPrior } : {} ),
398
+ ...( justEnacted > 0 ? { justEnacted } : {} ),
344
399
  planBias: ctx.planBias,
400
+ willBias: ctx.willBias,
345
401
  planId: ctx.planId,
346
402
  stepId: ctx.stepId,
347
403
  tick,
@@ -366,7 +422,9 @@ export class AffordanceSynthesizer implements CognitiveEngine {
366
422
  tags: a.tags,
367
423
  description: a.description,
368
424
  ...( a.availability !== undefined ? { availability: a.availability } : {} ),
425
+ ...( a.socialPrior !== undefined ? { socialPrior: a.socialPrior } : {} ),
369
426
  planBias: a.planBias,
427
+ willBias: a.willBias,
370
428
  planId: a.planId,
371
429
  stepId: a.stepId,
372
430
  tick: a.tick,
@@ -415,3 +473,34 @@ function str( v: unknown ): string | undefined {
415
473
  function clamp01( n: number ): number {
416
474
  return n < 0 ? 0 : n > 1 ? 1 : n
417
475
  }
476
+
477
+ /**
478
+ * The mind's learned read on one person, −1..1 (0 = unknown or neutral).
479
+ *
480
+ * Every input is something the mind formed from experience, none is a constant:
481
+ * • ReputationTracker's `trustworthiness`, centred on 0.5 and scaled by that
482
+ * model's own `confidence`, so an opinion held on two interactions pushes
483
+ * far less than one held on fifty.
484
+ * • the mind's current affective tone, as a gentle tilt — feeling low makes
485
+ * reaching for anyone a little less attractive, which is a mood, not a verdict
486
+ * on them, so it is weighted small and applies the same way to everybody.
487
+ *
488
+ * This is the path by which "they never answer me" reaches the competition: the
489
+ * ReputationTracker learns it from `interaction.occurred`, which only started firing
490
+ * once inbound conversation reached social cognition (#113).
491
+ */
492
+ function socialStanding( state: ReadonlySimulationState, keid: string ): number {
493
+ let trust = 0
494
+ for( const e of state.entities.values() ){
495
+ if( e.type !== 'reputation') continue
496
+ const m = e.metadata as Record<string, unknown> | undefined
497
+ if( m?.['keid'] !== keid ) continue
498
+ const t = num( m['trustworthiness'], 0.5 )
499
+ const c = clamp01( num( m['confidence'], 0 ) )
500
+ trust = ( t - 0.5 ) * 2 * c
501
+ break
502
+ }
503
+
504
+ const mood = num( state.metrics.get('affect.valence'), 0 ) * 0.25
505
+ return Math.max( -1, Math.min( 1, trust + mood ) )
506
+ }