@mindot/will 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +6985 -3337
  4. package/dist/index.js.map +1 -1
  5. package/dist/surface/channels/discord.d.ts +177 -0
  6. package/dist/surface/channels/discord.js +394 -0
  7. package/dist/surface/channels/discord.js.map +1 -0
  8. package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
  9. package/dist/{channels → surface/channels}/whatsapp.js +11 -5
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +5382 -1573
  12. package/dist/surface/cli.js.map +1 -0
  13. package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
  14. package/dist/{mcp → surface/mcp}/effectors.js +10 -4
  15. package/dist/surface/mcp/effectors.js.map +1 -0
  16. package/dist/{will-cS6k4uiJ.d.ts → will-evj9_vrd.d.ts} +6658 -5078
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +356 -1
  19. package/src/cognition/agency/conversation.aim.ts +292 -0
  20. package/src/cognition/agency/engines/action.selector.ts +142 -5
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +200 -7
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +509 -32
  24. package/src/cognition/agency/engines/reafference.engine.ts +160 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +5 -4
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/innate.ts +96 -1
  30. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  31. package/src/cognition/agency/selection.scoring.ts +49 -0
  32. package/src/cognition/agency/settlement.ts +203 -0
  33. package/src/cognition/agency/types.ts +73 -1
  34. package/src/cognition/cache/composition.ts +232 -0
  35. package/src/cognition/cache/deliberation.cache.ts +219 -0
  36. package/src/cognition/cache/fingerprint.ts +120 -0
  37. package/src/cognition/cache/types.ts +105 -0
  38. package/src/cognition/config.mirror.entities.ts +108 -1
  39. package/src/cognition/event.schemas.ts +22 -8
  40. package/src/cognition/faculties/affective.blender.ts +21 -4
  41. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  42. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  43. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  44. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  45. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  46. package/src/cognition/faculties/executive.engine/context.ts +106 -46
  47. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  48. package/src/cognition/faculties/executive.engine/engine.ts +714 -136
  49. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +230 -45
  50. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +320 -65
  51. package/src/cognition/faculties/executive.engine/facet.ts +81 -26
  52. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  53. package/src/cognition/faculties/executive.engine/parser.ts +120 -38
  54. package/src/cognition/faculties/executive.engine/prompt.factory.ts +331 -38
  55. package/src/cognition/faculties/executive.engine/types.ts +84 -12
  56. package/src/cognition/faculties/exteroception.ts +99 -98
  57. package/src/cognition/faculties/goal.manager.ts +144 -16
  58. package/src/cognition/faculties/known.entity.tracker.ts +287 -29
  59. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  60. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  61. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  62. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  63. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  64. package/src/cognition/faculties/self.model.updater.ts +19 -12
  65. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  66. package/src/cognition/faculties/social.perception.ts +47 -3
  67. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  68. package/src/cognition/faculties/working.memory.ts +16 -21
  69. package/src/cognition/identity.entity.ts +205 -0
  70. package/src/cognition/index.ts +7 -0
  71. package/src/cognition/instruction.handler.ts +1 -1
  72. package/src/cognition/memory/vector.adapter.ts +12 -3
  73. package/src/cognition/memory/vector.embedder.ts +45 -4
  74. package/src/cognition/percept.entity.ts +122 -0
  75. package/src/cognition/persona.prior.ts +6 -0
  76. package/src/cognition/sense.boundary.ts +176 -0
  77. package/src/cognition/senses/audition.engine/engine.ts +492 -53
  78. package/src/cognition/senses/base.sense.engine.ts +105 -7
  79. package/src/cognition/senses/index.ts +83 -13
  80. package/src/cognition/senses/provenance.ts +128 -0
  81. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  82. package/src/cognition/social.identity.ts +296 -0
  83. package/src/cognition/utilities/token.tracker.ts +94 -10
  84. package/src/core/orchestrator.ts +51 -0
  85. package/src/index.ts +39 -5
  86. package/src/llm/gate.ts +48 -0
  87. package/src/llm/index.ts +127 -33
  88. package/src/llm/routing.ts +6 -0
  89. package/src/llm/summarizer.ts +1 -1
  90. package/src/llm/wire.contracts.ts +57 -0
  91. package/src/pma/index.ts +68 -54
  92. package/src/stem/assembly.audit.ts +1 -0
  93. package/src/stem/guards/identity.coherence.ts +1 -1
  94. package/src/stem/index.ts +171 -23
  95. package/src/stem/mind.ts +198 -57
  96. package/src/stem/policy/arbiter.ts +10 -0
  97. package/src/stem/profiles/built-in.ts +7 -0
  98. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  99. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  100. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  101. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  102. package/src/{profiles → stem/profiles}/index.ts +1 -1
  103. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  104. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  105. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  106. package/src/stem/tracts/effector/types.ts +110 -0
  107. package/src/stem/tracts/effector.controller.ts +112 -401
  108. package/src/stem/tracts/outbox.controller.ts +30 -17
  109. package/src/stem/tracts/outbox.writer.ts +40 -2
  110. package/src/stem/tracts/sensory.controller.ts +6 -6
  111. package/src/stem/tracts/session.logger.ts +6 -1
  112. package/src/stem/tracts/transport/types.ts +20 -1
  113. package/src/stem/tracts/transport.controller.ts +33 -9
  114. package/src/surface/channels/discord.ts +609 -0
  115. package/src/{channels → surface/channels}/roster.ts +1 -1
  116. package/src/surface/channels/types.ts +136 -0
  117. package/src/{channels → surface/channels}/whatsapp.ts +22 -10
  118. package/src/{cli.ts → surface/cli.ts} +14 -9
  119. package/src/{host → surface/host}/boot.ts +3 -3
  120. package/src/{host → surface/host}/utterances.ts +2 -2
  121. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  122. package/src/{mcp → surface/mcp}/server.ts +19 -5
  123. package/src/{sdk → surface/sdk}/will.ts +194 -33
  124. package/src/{serve → surface/serve}/server.ts +11 -4
  125. package/src/types.ts +24 -3
  126. package/dist/channels/discord.d.ts +0 -69
  127. package/dist/channels/discord.js +0 -193
  128. package/dist/channels/discord.js.map +0 -1
  129. package/dist/channels/whatsapp.js.map +0 -1
  130. package/dist/cli.js.map +0 -1
  131. package/dist/mcp/effectors.js.map +0 -1
  132. package/src/channels/discord.ts +0 -214
  133. package/src/channels/types.ts +0 -46
  134. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
  135. package/src/extensions/livestream.ext.ts +0 -570
  136. package/src/extensions/time.ext.ts +0 -339
  137. package/src/profiles/built-in.ts +0 -7
  138. package/src/runners/coherence.runner.ts +0 -49
  139. package/src/runners/outreach.runner.ts +0 -155
  140. package/src/runners/social.runner.ts +0 -179
  141. package/src/runners/thin-shim.runner.ts +0 -205
  142. /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
@@ -0,0 +1,292 @@
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
+ /**
191
+ * When each person last spoke to the mind — the DURABLE version.
192
+ *
193
+ * `lastHeardByEntity` reads `conversation.received`, which lives exactly one
194
+ * tick: SocialPerception sweeps it as a one-shot event. So it answers "did
195
+ * someone speak to me THIS tick", never "when did they last speak to me".
196
+ *
197
+ * `answeredAt` is the durable half of the same fact, folded onto the mind's own
198
+ * `conversation.sent` turns by `resolveReplyExpectations` — it is what renders
199
+ * "they answered" in the prompt, and it survives snapshots.
200
+ *
201
+ * Written for the delivery-time re-check (`situationMoved`). That check first
202
+ * shipped keyed only on the mind having SPOKEN since composing, on the argument
203
+ * that nothing durable recorded the other direction. That was wrong — this is
204
+ * it — and the gap was the common case: live, a COO delivered a pre-composed
205
+ * agenda message two seconds after the person changed the subject, having said
206
+ * nothing in between, so the stale words arrived BEFORE her real reply and
207
+ * nothing was looking.
208
+ */
209
+ export function lastAnsweredByEntity( entities: ReadonlyMap<string, EntityLike> ): Map<string, Tick> {
210
+ const out = new Map<string, Tick>()
211
+ for( const [ , e ] of entities ){
212
+ if( e.type !== SENT_TYPE ) continue
213
+ const m = meta( e )
214
+ const target = str( m['targetEntityId'] )
215
+ const at = num( m['answeredAt'] )
216
+ if( !target || at === undefined ) continue
217
+ if( at > ( out.get( target ) ?? -Infinity ) ) out.set( target, at as Tick )
218
+ }
219
+ return out
220
+ }
221
+
222
+ /** A turn still in the air: said, not acknowledged-only, and not yet answered. */
223
+ export function isOpen( t: SpokenTurn ): boolean {
224
+ return !t.isAck && t.answeredAt === undefined
225
+ }
226
+
227
+ export interface Resolution {
228
+ /** Turns the target has now answered — fold `answeredAt` + what they said into them. */
229
+ answered: Array<{ turn: SpokenTurn; at: Tick; with: string }>
230
+ /** Turns whose reply window closed in silence, newly, this tick. */
231
+ unanswered: SpokenTurn[]
232
+ }
233
+
234
+ /**
235
+ * Fold the world's answer (or its absence) onto the turns still in the air.
236
+ *
237
+ * Pure: reads frozen state, writes nothing, decides nothing about what the mind
238
+ * should do next. A turn resolves exactly once — `answeredAt`/`unansweredAt` are
239
+ * the latches, so the announcement fires on one tick rather than every tick for
240
+ * the rest of the session.
241
+ *
242
+ * A silence is only reported once the window has fully elapsed. Before that the
243
+ * turn is simply open, which is a third state and the honest one: not yet
244
+ * answered is not the same as ignored, and collapsing them would have the mind
245
+ * conclude it was being snubbed one tick after speaking.
246
+ */
247
+ export function resolveReplyExpectations(
248
+ entities: ReadonlyMap<string, EntityLike>,
249
+ tick: Tick,
250
+ windowTicks: number = DEFAULT_REPLY_WINDOW_TICKS,
251
+ ): Resolution {
252
+ const turns = readSpokenTurns( entities )
253
+ const lastHeard = lastHeardByEntity( entities )
254
+
255
+ const answered: Resolution['answered'] = []
256
+ const unanswered: SpokenTurn[] = []
257
+
258
+ for( const t of turns ){
259
+ if( t.isAck || t.answeredAt !== undefined ) continue
260
+
261
+ const heard = lastHeard.get( t.targetEntityId )
262
+ if( heard !== undefined && heard.tick > t.tick ){
263
+ answered.push({ turn: t, at: heard.tick, with: heard.preview })
264
+ continue
265
+ }
266
+
267
+ // Already announced as a silence — stays open, but says nothing more.
268
+ if( t.unansweredAt !== undefined ) continue
269
+ // A turn stamped LATER than now came back with a restored snapshot whose tick
270
+ // counter has not caught up. Reading it as `tick - t.tick` negative would make
271
+ // it trivially "not yet due" forever, which is the benign direction, but a
272
+ // window <= 0 would then fire on everything at once. Guard both.
273
+ if( windowTicks > 0 && tick - t.tick >= windowTicks ) unanswered.push( t )
274
+ }
275
+
276
+ return { answered, unanswered }
277
+ }
278
+
279
+ /**
280
+ * The turns worth showing the mind: still in the air, newest first, capped.
281
+ *
282
+ * This is what makes repetition visible from the inside. `conversation.sent` has
283
+ * always been in state — 57 records on the Will above — and reached no prompt at
284
+ * all, so the mind's only view of having spoken was `✓ reach-out` under "Recent
285
+ * Action Outcomes": a tick mark, no words, no person, no silence.
286
+ */
287
+ export function openTurns(
288
+ entities: ReadonlyMap<string, EntityLike>,
289
+ limit: number = 6,
290
+ ): SpokenTurn[] {
291
+ return readSpokenTurns( entities ).filter( isOpen ).reverse().slice( 0, limit )
292
+ }
@@ -37,6 +37,7 @@ import {
37
37
  } from '#agency/selection.scoring'
38
38
  import { readEffectiveParams, readPersonaPrior } from '#cognition/persona.prior'
39
39
  import { RUPTURE_REVOKE_GATE, revocationEntity } from '#agency/revocation'
40
+ import { liveSettlements, settlementId } from '#agency/settlement'
40
41
  import { liveConsequences, matchConsequenceText } from '#agency/consequence'
41
42
  import { asFinality } from '#stem/policy/arbiter'
42
43
 
@@ -285,8 +286,23 @@ export class ActionSelector implements CognitiveEngine {
285
286
  catch( err ){ logger.warn(`[selector] rupture publish failed: ${ err instanceof Error ? err.message : String( err ) }`) }
286
287
  }
287
288
 
289
+ // A verdict is held only while the situation that produced it stands. A hard
290
+ // rupture means the world just contradicted the premises the mind decided
291
+ // on, so every standing settlement lapses and those questions are genuinely
292
+ // open again — the same reasoning that revokes a commitment still being
293
+ // weighed, applied to the ones already settled.
294
+ //
295
+ // Deliberately NOT conditioned on something being deliberated right now: the
296
+ // entire point of a settlement is that it outlives the intent that formed
297
+ // it, so it has to be able to lapse without one.
298
+ const settlementDrops = rupture >= RUPTURE_REVOKE_GATE
299
+ ? liveSettlements( state.entities, tick ).map( s => settlementId( s.schema, s.targetEntityId ) )
300
+ : []
301
+ const dropSettled = settlementDrops.length > 0 ? { delete: settlementDrops } : {}
302
+
288
303
  const busy = ( n: number ): EngineResult => ({
289
304
  commands: {
305
+ ...dropSettled,
290
306
  metrics: [
291
307
  [ 'agency.field.eligible', n ],
292
308
  [ 'agency.selection.busy', 1 ],
@@ -323,6 +339,7 @@ export class ActionSelector implements CognitiveEngine {
323
339
  return {
324
340
  commands: {
325
341
  set: [ revocationEntity( deliberating.id, deliberating.schema, revRupture, tick ) ],
342
+ ...dropSettled,
326
343
  metrics: [
327
344
  [ 'agency.field.eligible', eligible.length ],
328
345
  [ 'agency.selection.busy', 1 ],
@@ -340,6 +357,7 @@ export class ActionSelector implements CognitiveEngine {
340
357
  if( eligible.length === 0 )
341
358
  return {
342
359
  commands: {
360
+ ...dropSettled,
343
361
  metrics: [
344
362
  [ 'agency.field.eligible', 0 ],
345
363
  [ 'agency.selection.busy',( awaiting || composite ) ? 1 : 0 ],
@@ -371,6 +389,42 @@ export class ActionSelector implements CognitiveEngine {
371
389
  const winner = scored[0]
372
390
  if( !winner ) return busy( eligible.length )
373
391
 
392
+ // Whenever more than one person is a candidate for contact, record how the
393
+ // competition between them actually resolved. A Will that names someone and
394
+ // never messages them looks identical from outside whether the intention was
395
+ // never formed or was formed and beaten every cycle; this is the only place
396
+ // that difference is visible, and it is one line per contested tick.
397
+ // Why a WILLED contact did not happen. Gated on the executive having actually
398
+ // willed one (source 'ideomotor') that is not the winner — so a mind whose
399
+ // decision is being enacted logs nothing, and a mind that decided to contact
400
+ // someone and then did not says exactly what beat it.
401
+ //
402
+ // The earlier version of this gated on `reachers.length > 1`, assuming the
403
+ // failure was one addressee out-competing another. It is not: each master cycle
404
+ // sweeps every executive intent it did not name that cycle, so there is usually
405
+ // exactly ONE reach-out candidate in the field and the contest log never fired.
406
+ // The competition that matters is the willed contact against the rest of the
407
+ // field, which is what this prints.
408
+ // EVERY willed contact that lost, not just the strongest. An earlier version
409
+ // used `.find()`, which reports only the highest-scoring one — so with two
410
+ // live intents a second loser went unlogged, and the log would have quietly
411
+ // understated how many people the mind decided to contact and then did not.
412
+ const lostReaches = scored.filter( s =>
413
+ s !== winner
414
+ && s.affordance.schema === 'reach-out'
415
+ && s.affordance.source === 'ideomotor'
416
+ && s.affordance.targetEntityId )
417
+
418
+ for( const lost of lostReaches )
419
+ logger.info(
420
+ `[selector] willed reach-out → ${ lost.affordance.targetEntityId } NOT selected: ` +
421
+ `${ lost.activation.toFixed( 3 ) }` +
422
+ `${ lost.affordance.justEnacted ? ` (justEnacted ${ lost.affordance.justEnacted.toFixed( 2 ) })` : '' }` +
423
+ ` < ${ winner.affordance.schema }` +
424
+ `${ winner.affordance.targetEntityId ? `→${ winner.affordance.targetEntityId }` : '' }` +
425
+ ` ${ winner.activation.toFixed( 3 ) }`
426
+ )
427
+
374
428
  // ── Preempt a mid-composite routine (IMMEDIATE SWITCH) ────────
375
429
  // A strong/high-stakes challenger cuts the routine off AND takes the body
376
430
  // the same tick. We cannot delete the parent here: the executor runs later
@@ -426,12 +480,37 @@ export class ActionSelector implements CognitiveEngine {
426
480
  const sameAction = winner.affordance.schema === awaiting.schema && ( winner.affordance.targetEntityId ?? '') === awaiting.target
427
481
  if( sameAction ) return busy( eligible.length ) // field still favours what we await
428
482
 
429
- const staleness = Math.min( 1, ( tick - awaiting.dispatchedAt ) / AWAIT_STALE_TICKS )
483
+ // Clamped at BOTH ends. The upper bound was always there; the lower one matters
484
+ // because a restored intent's age is negative (state snapshots, the tick counter
485
+ // restarts at 1), and an unclamped negative staleness flips the decay into
486
+ // amplification: `1 - (-39 × 0.5)` = 20.6×, turning a 0.47 incumbent into 9.74 and
487
+ // making it permanently unpreemptable. The executor now clears such intents, so
488
+ // this is the second line of defence rather than the fix — but an incumbent's
489
+ // hysteresis must never be able to exceed its own recorded activation, whatever
490
+ // arithmetic feeds it.
491
+ const staleness = Math.min( 1, Math.max( 0, ( tick - awaiting.dispatchedAt ) / AWAIT_STALE_TICKS ) )
430
492
  const incumbentStrength = awaiting.activation * ( 1 - staleness * STALE_DECAY )
431
493
  const switchCost = effSwitchCost * ( 1 - stakes( winner.affordance, bias ) )
432
494
 
433
- if( winner.activation <= incumbentStrength + switchCost )
495
+ if( winner.activation <= incumbentStrength + switchCost ){
496
+ // A challenger aimed at a DIFFERENT person losing to an awaiting incumbent
497
+ // is the starvation shape: every proactive reach-out sits 'awaiting' while a
498
+ // facet authors its words (8–18s = many ticks), and the incumbent's recorded
499
+ // activation does not decay with the challenger's damping. If one addressee
500
+ // can hold the channel this way, a Will can decide to contact someone else
501
+ // every cycle and never once do it — which is exactly the report.
502
+ if( winner.affordance.schema === 'reach-out'
503
+ && winner.affordance.targetEntityId
504
+ && winner.affordance.targetEntityId !== awaiting.target )
505
+ logger.info(
506
+ `[selector] reach-out → ${ winner.affordance.targetEntityId } BLOCKED by awaiting ` +
507
+ `${ awaiting.schema } → ${ awaiting.target || '—' }: ` +
508
+ `challenger ${ winner.activation.toFixed( 3 ) } ≤ incumbent ${ incumbentStrength.toFixed( 3 ) } ` +
509
+ `+ switch ${ switchCost.toFixed( 3 ) } (awaiting ${ tick - awaiting.dispatchedAt } ticks)`
510
+ )
511
+
434
512
  return busy( eligible.length ) // not worth interrupting — keep waiting
513
+ }
435
514
 
436
515
  preemptDelete = awaiting.id // PREEMPT — fall through and commit the challenger
437
516
  preemptedFrom = awaiting.schema
@@ -478,7 +557,12 @@ export class ActionSelector implements CognitiveEngine {
478
557
  targetEntityId: winner.affordance.targetEntityId,
479
558
  parameters: winner.affordance.parameters,
480
559
  source: winner.affordance.source,
481
- // Plan provenance (when a plan's frontier-step prior won the competition)
560
+ // What evoked this for an ideomotor winner, the `ideomotor.intent` entity
561
+ // the executive wrote. Carried so the executor can DISCHARGE it once the act
562
+ // happens: an intention that has been acted on is no longer an intention, and
563
+ // nothing was deleting these. See MotorSchemaExecutor._dischargeWill.
564
+ ...( winner.affordance.evokedBy ? { evokedBy: winner.affordance.evokedBy } : {} ),
565
+ // The plan link (when a plan's frontier-step prior won the competition) —
482
566
  // flows through the executor's action.outcome so the PlanningEngine advances.
483
567
  ...( winner.affordance.planId ? { planId: winner.affordance.planId } : {} ),
484
568
  ...( winner.affordance.stepId ? { stepId: winner.affordance.stepId } : {} ),
@@ -562,7 +646,9 @@ export class ActionSelector implements CognitiveEngine {
562
646
  set: compositeTombstone
563
647
  ? [ intent, revocationEntity( compositeTombstone, compositeFrom ?? '', rupture, tick ) ]
564
648
  : [ intent ],
565
- ...( preemptDelete ? { delete: [ preemptDelete ] } : {} ),
649
+ ...( preemptDelete || settlementDrops.length > 0
650
+ ? { delete: [ ...( preemptDelete ? [ preemptDelete ] : [] ), ...settlementDrops ] }
651
+ : {} ),
566
652
  metrics: [
567
653
  [ 'agency.field.eligible', eligible.length ],
568
654
  [ 'agency.selection.busy', 0 ],
@@ -619,6 +705,14 @@ function computeRupture(
619
705
  for( const e of state.entities.values() ){
620
706
  if( e.type !== 'percept') continue
621
707
  const m = e.metadata
708
+ // Deliberately NOT asProvenance(). Absence is load-bearing here and only
709
+ // here: an UNTAGGED percept is excluded from rupture, and three writers
710
+ // are untagged — `escalation.buffer` (x2) and the wake percept in
711
+ // `stem/index.ts`. Normalizing would default them to 'exafferent' and make
712
+ // them rupture-eligible. That is a real behaviour change, and probably the
713
+ // right one — a mind waking after hours offline currently cannot be
714
+ // ruptured by that fact — but it belongs to the phase that tags those
715
+ // writers, with tests. See .TODO/SIGNAL_BOUNDARY.md.
622
716
  if( str( m?.['provenance'] ) !== 'exafferent') continue
623
717
  const pTick = num( m?.['tick'], -1 )
624
718
  if( pTick < 0 || tick - pTick > RUPTURE_WINDOW_TICKS ) continue
@@ -677,6 +771,19 @@ function effectiveWeights( state: ReadonlySimulationState ): ScoreWeights {
677
771
  ...DEFAULT_WEIGHTS,
678
772
  risk: Math.max( 0, num( p['riskWeight'], DEFAULT_WEIGHTS.risk ) ),
679
773
  novelty: Math.max( 0, num( p['noveltyWeight'], DEFAULT_WEIGHTS.novelty ) ),
774
+ // How long the mind sits with something it has already said before saying it
775
+ // again. Agreeableness raises it (does not badger); demonstrated persistence
776
+ // lowers it (follows up sooner). Clamped at 0 so a prior can flatten the
777
+ // damping into indifference but never turn repeating into a *reward*.
778
+ repeat: Math.max( 0, num( p['repeatDamping'], DEFAULT_WEIGHTS.repeat ) ),
779
+ // How much the mind's learned read on a person biases acting toward them.
780
+ // Deliberately NOT clamped at 0 — this weight is genuinely signed territory,
781
+ // and it is the tenant's to occupy. A warm, reciprocal mind leans toward
782
+ // whoever answers; a dogged, duty-bound one chases a silence precisely
783
+ // because it is silent. Both are coherent people. The container supplies the
784
+ // term and the persona supplies the sign; hardcoding it here would be the
785
+ // container deciding what kind of colleague every tenant has to be.
786
+ social: num( p['socialWeight'], DEFAULT_WEIGHTS.social ),
680
787
  }
681
788
  }
682
789
 
@@ -713,7 +820,31 @@ function buildBias( state: ReadonlySimulationState ): BiasContext {
713
820
 
714
821
  // ─── entity decoding ─────────────────────────────────────────────────────────
715
822
 
716
- function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<string, unknown> | undefined ): Affordance {
823
+ /**
824
+ * Decode an `affordance` entity back into the thing the competition scores.
825
+ *
826
+ * This is the ONE hop between synthesis and selection, and three fields did not
827
+ * cross it — each the payload of a mechanism that therefore never ran:
828
+ *
829
+ * • `justEnacted` — `scoreAffordance` subtracts `w.repeat × justEnacted`, the
830
+ * largest damping weight in the competition (0.30). Undefined here meant ×0
831
+ * always, so no act has ever been damped for having just been done. EXAFFERENCE
832
+ * P5 and the `spokenAt` durability fix both landed on a field nothing read.
833
+ * • `availability` — POLICY_REAFFERENCE P2 damps a refused ability so it competes
834
+ * weakly instead of vanishing. `a.availability ?? 1` read the fallback forever.
835
+ * • `settled` — a verdict System 2 already reached. Same weight class as
836
+ * `justEnacted` and the same failure if dropped: the mind re-deliberates a
837
+ * question it has answered, forever. Added WITH its decoder rather than
838
+ * after someone notices the mechanism is dark.
839
+ * • `description` — the schema's MEANING. The selector snapshots it onto
840
+ * `metadata.candidates` and the DeliberationEngine renders it as `— <what>`,
841
+ * so the candidate list the mind reasons over listed acts with no meanings.
842
+ *
843
+ * Exported for the round-trip guard: what synthesis writes, selection must read.
844
+ * Four fields in this arc have been carried most of the way and lost at the last
845
+ * hop, so the property is pinned rather than re-checked by eye.
846
+ */
847
+ export function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<string, unknown> | undefined ): Affordance {
717
848
  const meta = ( m ?? {} ) as Record<string, unknown>
718
849
  return {
719
850
  id,
@@ -729,6 +860,12 @@ function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<st
729
860
  available: meta['available'] === true,
730
861
  tags: Array.isArray( meta['tags'] ) ? ( meta['tags'] as unknown[] ).filter( ( t ): t is string => typeof t === 'string') : [],
731
862
  planBias: typeof meta['planBias'] === 'number' ? ( meta['planBias'] as number ) : undefined,
863
+ willBias: typeof meta['willBias'] === 'number' ? ( meta['willBias'] as number ) : undefined,
864
+ socialPrior: typeof meta['socialPrior'] === 'number' ? ( meta['socialPrior'] as number ) : undefined,
865
+ justEnacted: typeof meta['justEnacted'] === 'number' ? ( meta['justEnacted'] as number ) : undefined,
866
+ settled: typeof meta['settled'] === 'number' ? ( meta['settled'] as number ) : undefined,
867
+ availability: typeof meta['availability'] === 'number' ? ( meta['availability'] as number ) : undefined,
868
+ description: str( meta['description'] ),
732
869
  planId: str( meta['planId'] ),
733
870
  stepId: str( meta['stepId'] ),
734
871
  tick: num( meta['tick'], 0 ),