@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
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@mindot/will",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "author": "Fabrice <fabrice8@github.com>",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/mindot-ai/will.git"
8
8
  },
9
9
  "bin": {
10
- "will": "dist/cli.js"
10
+ "will": "dist/surface/cli.js"
11
11
  },
12
12
  "main": "./dist/index.js",
13
13
  "devDependencies": {
@@ -32,16 +32,16 @@
32
32
  "types": "./dist/index.d.ts"
33
33
  },
34
34
  "./mcp": {
35
- "import": "./dist/mcp/effectors.js",
36
- "types": "./dist/mcp/effectors.d.ts"
35
+ "import": "./dist/surface/mcp/effectors.js",
36
+ "types": "./dist/surface/mcp/effectors.d.ts"
37
37
  },
38
38
  "./discord": {
39
- "import": "./dist/channels/discord.js",
40
- "types": "./dist/channels/discord.d.ts"
39
+ "import": "./dist/surface/channels/discord.js",
40
+ "types": "./dist/surface/channels/discord.d.ts"
41
41
  },
42
42
  "./whatsapp": {
43
- "import": "./dist/channels/whatsapp.js",
44
- "types": "./dist/channels/whatsapp.d.ts"
43
+ "import": "./dist/surface/channels/whatsapp.js",
44
+ "types": "./dist/surface/channels/whatsapp.d.ts"
45
45
  }
46
46
  },
47
47
  "bugs": {
@@ -78,11 +78,10 @@
78
78
  "scripts": {
79
79
  "build": "tsup",
80
80
  "build:types": "tsc --project tsconfig.build.json",
81
- "dev": "bun --env-file=.env run src/runners/thin-shim.runner.ts",
81
+ "dev": "bun --env-file=.env run examples/thin-shim.ts",
82
82
  "dev:build": "tsup --watch",
83
- "run:social": "bun --env-file=.env run src/runners/social.runner.ts",
84
- "run:outreach": "bun --env-file=.env run src/runners/outreach.runner.ts",
85
- "start": "bun --env-file=.env run src/runners/thin-shim.runner.ts",
83
+ "run:social": "bun --env-file=.env run examples/social.ts",
84
+ "run:outreach": "bun --env-file=.env run examples/outreach.ts",
86
85
  "typecheck": "tsc --noEmit",
87
86
  "lint": "tsc --noEmit",
88
87
  "prepublishOnly": "bun run typecheck && bun test && bun run build",
@@ -52,6 +52,26 @@ export interface ConsequenceDescriptor {
52
52
  text?: string
53
53
  /** FNV-1a over the canonicalized parameters (async dispatch). */
54
54
  paramsHash?: number
55
+ /**
56
+ * Dispatched but NOT YET PERFORMED — a communicate held awaiting words that do
57
+ * not exist yet.
58
+ *
59
+ * The footprint is written when an intent goes to the async hold, which for an
60
+ * EXTERNAL effector is the act itself (the host is doing it now, and asking
61
+ * twice while waiting is the thing satiation should prevent). For a communicate
62
+ * it is not: the facet has not authored anything, and the mind has said nothing.
63
+ *
64
+ * Counting it as satiation meant ATTEMPTING to speak silenced the mind exactly
65
+ * as hard as speaking. Dormant until `justEnacted` first reached the
66
+ * competition, then immediately visible: a live COO willed `reach-out` 63 times
67
+ * in four minutes, delivered NOTHING, and 18 of those lost to `justEnacted`
68
+ * readings of 0.93–0.98 against a mind that had not spoken once that run. Each
69
+ * attempt wrote a fresh footprint, so the damping never decayed.
70
+ *
71
+ * P2's echo matching is unaffected — a pending descriptor carries no text to
72
+ * match on, which is the same fact from the other side.
73
+ */
74
+ pending?: boolean
55
75
  expiresAt: Tick
56
76
  tick: Tick
57
77
  }
@@ -90,6 +110,80 @@ export function consequenceEntity( d: ConsequenceDescriptor ): EntityInput {
90
110
  }
91
111
  }
92
112
 
113
+ // ── The durable enaction record — satiation's third source ───────────────────
114
+ //
115
+ // Descriptors are swept at the ECHO window (`CONSEQUENCE_TTL_TICKS`, 30) because
116
+ // that answers "could the world still be replying to me". Satiation asks a
117
+ // different and longer question (`repeatWindowTicks`, 60 by default), so the
118
+ // second half of the satiation window had no carrier at all and silently damped
119
+ // nothing.
120
+ //
121
+ // The codebase already hit this exact wall twice and closed it twice:
122
+ // • speaking → `conversation.sent`, durable, read as `spokenAt`
123
+ // • objectless → `LearnedSkill.lastEnactedTick`, durable, read as `selfEnactedAt`
124
+ //
125
+ // The gap left was an act WITH an object that is NOT speech — `inspect`, and
126
+ // every entity-bound host effector. `lastEnactedTick` cannot serve it: it is
127
+ // per-schema, so "I just looked up Alice" would damp looking up Bob, which is
128
+ // why the objectless branch is the only place it is consulted. So this is the
129
+ // missing peer: per (schema, target), durable, on the satiation window.
130
+ //
131
+ // Measured on a live COO before this existed: entity-bound acts carried
132
+ // satiation in 40–46% of competitions against 100% for objectless ones, and she
133
+ // re-enacted `discord_lookup_member` toward the same person 33 times in seven
134
+ // minutes.
135
+ export const ENACTED_TYPE = 'agency.enacted'
136
+
137
+ /** Stable id — one record per (schema, target), refreshed in place, so the set is
138
+ * bounded by the pairs actually enacted rather than growing per enaction. */
139
+ export function enactedId( schema: string, targetEntityId: string ): string {
140
+ return `agency-enacted-${ schema }-${ targetEntityId }`
141
+ }
142
+
143
+ /** Build/refresh the durable "I did this to them, then" record. */
144
+ export function enactedEntity( schema: string, targetEntityId: string, tick: Tick ): EntityInput {
145
+ return {
146
+ id: enactedId( schema, targetEntityId ),
147
+ type: ENACTED_TYPE,
148
+ metadata: { schema, targetEntityId, tick },
149
+ }
150
+ }
151
+
152
+ /**
153
+ * When this mind last enacted each (schema, target) pair. Keyed
154
+ * `schema\u0000target` — NUL, because neither half can contain it, so a pair
155
+ * cannot be forged by a schema id that happens to contain the separator.
156
+ */
157
+ export function enactedAtBySchemaTarget(
158
+ entities: ReadonlyMap<string, { type: string; metadata?: ReadonlyMap<string, unknown> | Record<string, unknown> }>,
159
+ /**
160
+ * alias id → anchor id, when the caller has an alias table.
161
+ *
162
+ * A FUNCTION rather than the table itself because `social.identity` already
163
+ * imports `fnv1a` from this file, so importing `canonicalOf` back would close a
164
+ * cycle. The caller owns the id space and hands down the resolution; absent, ids
165
+ * are used as they were written, which is exactly the previous behaviour.
166
+ */
167
+ canon?: ( id: string ) => string,
168
+ ): Map<string, number> {
169
+ const out = new Map<string, number>()
170
+ for( const [ , e ] of entities ){
171
+ if( e.type !== ENACTED_TYPE ) continue
172
+ const meta = ( e.metadata ?? {} ) as Record<string, unknown>
173
+ const schema = typeof meta['schema'] === 'string' ? meta['schema'] as string : undefined
174
+ const target = typeof meta['targetEntityId'] === 'string' ? meta['targetEntityId'] as string : undefined
175
+ const tick = typeof meta['tick'] === 'number' ? meta['tick'] as number : undefined
176
+ if( !schema || !target || tick === undefined ) continue
177
+ out.set( enactedKey( schema, canon ? canon( target ) : target ), tick )
178
+ }
179
+ return out
180
+ }
181
+
182
+ /** The map key for a (schema, target) pair. */
183
+ export function enactedKey( schema: string, targetEntityId: string ): string {
184
+ return `${ schema }\u0000${ targetEntityId }`
185
+ }
186
+
93
187
  /** Read a descriptor back off entity metadata (P2 matcher + tests). */
94
188
  export function readConsequence(
95
189
  m: ReadonlyMap<string, unknown> | Record<string, unknown> | undefined,
@@ -106,6 +200,9 @@ export function readConsequence(
106
200
  textHash: typeof meta['textHash'] === 'number' ? meta['textHash'] as number : undefined,
107
201
  text: typeof meta['text'] === 'string' ? meta['text'] as string : undefined,
108
202
  paramsHash: typeof meta['paramsHash'] === 'number' ? meta['paramsHash'] as number : undefined,
203
+ // Decoded, not just written — a field only one side knows about is the shape
204
+ // of defect this codebase has hit five times now.
205
+ pending: meta['pending'] === true ? true : undefined,
109
206
  expiresAt: typeof meta['expiresAt'] === 'number' ? meta['expiresAt'] as number : 0,
110
207
  tick: typeof meta['tick'] === 'number' ? meta['tick'] as number : 0,
111
208
  }
@@ -117,12 +214,36 @@ export function readConsequence(
117
214
  export function liveConsequences(
118
215
  entities: ReadonlyMap<string, { type: string; metadata?: ReadonlyMap<string, unknown> | Record<string, unknown> }>,
119
216
  tick: Tick,
217
+ /**
218
+ * alias id → anchor id, when the caller has an alias table.
219
+ *
220
+ * A FUNCTION rather than the table itself because `social.identity` already
221
+ * imports `fnv1a` from this file, so importing `canonicalOf` back would close a
222
+ * cycle. The caller owns the id space and hands down the resolution; absent, ids
223
+ * are used as they were written, which is exactly the previous behaviour.
224
+ */
225
+ canon?: ( id: string ) => string,
120
226
  ): ConsequenceDescriptor[] {
121
227
  const out: ConsequenceDescriptor[] = []
122
228
  for( const [ , e ] of entities ){
123
229
  if( e.type !== CONSEQUENCE_TYPE ) continue
124
230
  const d = readConsequence( e.metadata )
125
- if( d && tick < d.expiresAt ) out.push( d )
231
+ if( !d ) continue
232
+ // A descriptor stamped LATER than now came from a previous session: descriptors
233
+ // snapshot with the state, and the tick counter restarts at 1 on wake. Without
234
+ // this the expiry test (`tick < expiresAt`) reads every restored descriptor as
235
+ // freshly live — measured: a Will woke at tick 1 holding three reach-out
236
+ // descriptors stamped 575–596 with expiry 605–626, so for the WHOLE session it
237
+ // believed it had just messaged that person moments ago. It damped every
238
+ // reach-out to them and delivered nothing, and the P2 matcher would equally
239
+ // have attenuated their genuine replies as its own echo.
240
+ if( d.tick > tick ) continue
241
+ if( tick < d.expiresAt )
242
+ // Canonical, so a descriptor written against a transport id and a candidate
243
+ // aimed at the anchor are recognised as the same act toward the same person.
244
+ out.push( canon && d.targetEntityId
245
+ ? { ...d, targetEntityId: canon( d.targetEntityId ) }
246
+ : d )
126
247
  }
127
248
  return out.sort( ( a, b ) => ( a.intentId < b.intentId ? -1 : a.intentId > b.intentId ? 1 : 0 ) )
128
249
  }
@@ -148,6 +269,240 @@ export function matchConsequenceText(
148
269
  return null
149
270
  }
150
271
 
272
+ // ── P5: the act's own footprint, felt as satiation ───────────────────────────
273
+
274
+ /**
275
+ * How much of an act's own footprint is still live for this (schema, target),
276
+ * 1 → 0 as the descriptor ages out. 0 when nothing matching is in flight.
277
+ *
278
+ * This is the consumer #112 always described and the descriptors never had. A
279
+ * delivered message wrote its footprint here and nothing ever read it back, so
280
+ * the standing `ideomotor.intent` that produced it kept re-winning the
281
+ * competition and the same words went out again — observed three times, ~21
282
+ * ticks apart, to the same person, while the mind believed each was the first.
283
+ *
284
+ * Deliberately a decaying quantity, not a lock. Right after acting it is ~1 and
285
+ * the pull to repeat is strongly damped; as the window in which the world could
286
+ * still answer runs out it returns to 0 and the appetite comes back on its own.
287
+ * That is a refractory period, not a rule: the mind may still repeat itself if
288
+ * something has become pressing enough to out-compete the damping, which is
289
+ * exactly what a person does when the silence starts to matter.
290
+ *
291
+ * Matches on (schema, target) rather than on the words, because at the moment
292
+ * the competition runs the words do not exist yet — a facet authors them later.
293
+ * "I have just reached out to this person and do not yet know how it landed" is
294
+ * the honest predicate, and it is the one that governs whether to speak again.
295
+ */
296
+ export function enactionFootprint(
297
+ descriptors: readonly ConsequenceDescriptor[],
298
+ schema: string,
299
+ targetEntityId: string | undefined,
300
+ tick: Tick,
301
+ windowTicks: number = CONSEQUENCE_TTL_TICKS,
302
+ /**
303
+ * Tick of the last thing SAID to each entity, from `conversation.sent`.
304
+ *
305
+ * Descriptors cannot carry satiation on their own: the executor deletes them at
306
+ * `tick >= expiresAt` (motor.schema.executor's TTL sweep), so nothing survives
307
+ * past the echo window and a satiation window longer than it was silently a
308
+ * no-op. Observed live: the same relay delivered twice with `justEnacted` never
309
+ * once reaching the competition.
310
+ *
311
+ * `conversation.sent` is the durable record of having spoken — written by
312
+ * ProactiveCommunicator at push time, snapshotted with the state, and already
313
+ * what discharges an undertaking. Keyed by person rather than by schema on
314
+ * purpose: having just spoken to someone damps speaking to them again, however
315
+ * that speaking happens to be schematised.
316
+ */
317
+ spokenAt?: ReadonlyMap<string, number>,
318
+ /**
319
+ * Tick this schema was last enacted — `LearnedSkill.lastEnactedTick`, used for
320
+ * an act with NO OBJECT, where "I have just done this" is the whole of the fact
321
+ * and there is no target to key on.
322
+ *
323
+ * Without it the innate floor could not satiate at all. Every objectless stance
324
+ * returned here at the first line, so `repeat` — the largest damping weight in
325
+ * the competition at 0.30 — was structurally zero for `orient · attend · rest ·
326
+ * withdraw · reflect · wait · express`, while `habit` (+0.20) outweighed the
327
+ * novelty it spends (−0.10). A stance that won once therefore won harder next
328
+ * tick, forever. Measured: `express` at habit 1.0 taking 144 of 145 decisions
329
+ * across 300 quiet ticks, with nothing in the mind able to represent that it had
330
+ * just done it.
331
+ *
332
+ * The skill record is the right source for the same reason `conversation.sent`
333
+ * is for speaking: descriptors are swept at the echo TTL, and `agency.outcome`
334
+ * is consumed and deleted the tick after it is written, so neither survives a
335
+ * satiation window. `lastEnactedTick` is durable and snapshotted.
336
+ */
337
+ selfEnactedAt?: number,
338
+ /**
339
+ * Tick this mind last SPOKE — to anyone. Supplied only for a speaking act, and
340
+ * satiating over the shorter echo window rather than the full repeat window.
341
+ *
342
+ * Satiation was keyed entirely by listener, so "I have already said this" had
343
+ * no bearing once the listener changed. Live, a COO drafted one scoping
344
+ * outline and put substantially the same thing into three destinations inside
345
+ * three minutes — general, a DM, a second channel — with nothing damping it,
346
+ * because each was a different key. She could SEE it: `## What I've Said
347
+ * Lately` listed her own turns across all targets and she posted again 31
348
+ * seconds later. The record was there; the pull had no opposing force.
349
+ *
350
+ * Deliberately WEAKER than the per-person arm, via the shorter window: telling
351
+ * a second person something is legitimate and must stay cheap, while saying it
352
+ * again to someone who already heard it should not be. And deliberately from
353
+ * `conversation.sent` — what was actually SAID — not from the schema's
354
+ * `lastEnactedTick`, which also counts attempts that authored no words and
355
+ * would let a run of declined outreach satiate the mind into silence.
356
+ *
357
+ * Replies are unaffected: an answer to an inbound message is delivered by the
358
+ * audition facet through the outbox and never enters this competition. This
359
+ * damps only self-initiated outreach — which is exactly what broadcasting is.
360
+ */
361
+ spokeAnywhereAt?: number,
362
+ /**
363
+ * Tick this (schema, target) pair was last enacted, from the durable
364
+ * `agency.enacted` records — the peer of `spokenAt` for an act that has an
365
+ * object but is not speech (`inspect`, every entity-bound host effector).
366
+ *
367
+ * Without it those acts carried satiation only while a consequence descriptor
368
+ * survived, i.e. the echo window, so the back half of the satiation window
369
+ * damped nothing — measured at 40–46% coverage against 100% for objectless
370
+ * acts, with a live COO re-looking-up the same person 33 times in 7 minutes.
371
+ * Keyed by PAIR, not by schema, so damping "look up Alice" leaves "look up
372
+ * Bob" untouched — which is precisely why `selfEnactedAt` cannot serve here.
373
+ */
374
+ enactedAt?: ReadonlyMap<string, number>,
375
+ ): number {
376
+ if( windowTicks <= 0 ) return 0
377
+
378
+ if( !targetEntityId ){
379
+ // BOTH arms, for the same reason an act with an object has both.
380
+ //
381
+ // `selfEnactedAt` is a skill record, and a skill record is written when the
382
+ // outcome is RECONCILED — for a host effector that is an async ack landing
383
+ // some ticks after the act. In the window between dispatching and learning,
384
+ // satiation read the PREVIOUS enaction and damped almost nothing, while the
385
+ // descriptor saying "I did this on the tick just gone" sat one branch away,
386
+ // written at dispatch and never read: the objectless case returned before
387
+ // the loop that reads it.
388
+ //
389
+ // Measured live on a COO: `discord_server_snapshot` — objectless, cheap,
390
+ // always succeeds — repeated at a gap of THREE ticks in 26 of 47 intervals,
391
+ // while every entity-bound effector in the same run sat at 30–60 under the
392
+ // same weights. Those were not damped harder, they were damped SOONER,
393
+ // because a descriptor exists from the enacting tick and a skill record does
394
+ // not. Half the repeats were slipping through the reconciliation window.
395
+ let strongest = selfEnactedAt === undefined
396
+ ? 0
397
+ : ( windowTicks - ( tick - selfEnactedAt ) ) / windowTicks
398
+
399
+ for( const d of descriptors ){
400
+ // Objectless descriptors only. A `look up Alice` must never satiate the
401
+ // objectless form of the same schema — that is the asymmetry `enactedAt`
402
+ // is keyed by PAIR to avoid, and it would read as the mind refusing to do
403
+ // a thing in general because it once did it to someone.
404
+ if( d.schema !== schema || d.targetEntityId !== undefined ) continue
405
+ if( d.pending ) continue
406
+ const remaining = ( windowTicks - ( tick - d.tick ) ) / windowTicks
407
+ if( remaining > strongest ) strongest = remaining
408
+ }
409
+
410
+ return strongest < 0 ? 0 : strongest > 1 ? 1 : strongest
411
+ }
412
+
413
+ let strongest = 0
414
+
415
+ const enacted = enactedAt?.get( enactedKey( schema, targetEntityId ) )
416
+ if( enacted !== undefined ){
417
+ const remaining = ( windowTicks - ( tick - enacted ) ) / windowTicks
418
+ if( remaining > strongest ) strongest = remaining
419
+ }
420
+
421
+ const spoken = spokenAt?.get( targetEntityId )
422
+ if( spoken !== undefined ){
423
+ const remaining = ( windowTicks - ( tick - spoken ) ) / windowTicks
424
+ if( remaining > strongest ) strongest = remaining
425
+ }
426
+
427
+ // Having just spoken AT ALL — the arm that is not about the listener. Over the
428
+ // echo window, so it is real but short: a second person is cheap to tell, a
429
+ // third destination in ninety seconds is not. See `spokeAnywhereAt`.
430
+ if( spokeAnywhereAt !== undefined ){
431
+ const remaining = ( CONSEQUENCE_TTL_TICKS - ( tick - spokeAnywhereAt ) ) / CONSEQUENCE_TTL_TICKS
432
+ if( remaining > strongest ) strongest = remaining
433
+ }
434
+
435
+ for( const d of descriptors ){
436
+ if( d.schema !== schema || d.targetEntityId !== targetEntityId ) continue
437
+ // You cannot be satiated by something you have not done. See `pending`.
438
+ if( d.pending ) continue
439
+ // Measured from when the act happened, against the SATIATION window — not from
440
+ // the descriptor's own expiry, which is the echo window and a different
441
+ // question. Conflating them meant "how long before I say this again" was
442
+ // pinned to "how long until the world's reply could still arrive", and the
443
+ // second is necessarily short. Observed: re-deliveries landing ~25 ticks
444
+ // apart against a 30-tick echo TTL, so the damping had decayed to almost
445
+ // nothing exactly when the next impulse arrived.
446
+ const elapsed = tick - d.tick
447
+ const remaining = ( windowTicks - elapsed ) / windowTicks
448
+ if( remaining > strongest ) strongest = remaining
449
+ }
450
+
451
+ return strongest < 0 ? 0 : strongest > 1 ? 1 : strongest
452
+ }
453
+
454
+ /**
455
+ * When the mind last SAID something to each person, from the durable
456
+ * `conversation.sent` records. Feeds `enactionFootprint`'s satiation.
457
+ *
458
+ * Unlike consequence descriptors these are never swept, so satiation can run on
459
+ * whatever window the persona wants rather than being capped by the echo TTL.
460
+ */
461
+ export function spokenAtByEntity(
462
+ entities: ReadonlyMap<string, { type: string; updatedAtTick?: number; tick?: number; metadata?: ReadonlyMap<string, unknown> | Record<string, unknown> }>,
463
+ /**
464
+ * alias id → anchor id, when the caller has an alias table.
465
+ *
466
+ * A FUNCTION rather than the table itself because `social.identity` already
467
+ * imports `fnv1a` from this file, so importing `canonicalOf` back would close a
468
+ * cycle. The caller owns the id space and hands down the resolution; absent, ids
469
+ * are used as they were written, which is exactly the previous behaviour.
470
+ */
471
+ canon?: ( id: string ) => string,
472
+ ): Map<string, number> {
473
+ const out = new Map<string, number>()
474
+
475
+ for( const [ , e ] of entities ){
476
+ if( e.type !== 'conversation.sent') continue
477
+ const m = ( e.metadata ?? {} ) as Record<string, unknown>
478
+ const target = typeof m['targetEntityId'] === 'string' ? m['targetEntityId'] as string : undefined
479
+ if( !target ) continue
480
+ // `metadata.tick` where the writer set one, else the entity's own tick, which
481
+ // StateManager.setEntity stamps from the sim clock on every write. The fallback
482
+ // matters: not every path that records having spoken fills the metadata field,
483
+ // and defaulting to 0 made every record look infinitely old.
484
+ // `updatedAtTick` is the field `StateManager.setEntity` actually stamps.
485
+ // This read `e.tick`, which does not exist on SimulationEntity — so the
486
+ // fallback the comment above describes has never once fired, and any writer
487
+ // that omitted `metadata.tick` was treated as having spoken at tick 0, i.e.
488
+ // infinitely long ago, i.e. not satiating at all.
489
+ const at = typeof m['tick'] === 'number' ? m['tick'] as number
490
+ : typeof e.updatedAtTick === 'number' ? e.updatedAtTick
491
+ : typeof e.tick === 'number' ? e.tick
492
+ : 0
493
+ // Through the alias table, for the reason `readSpokenTurns` states plainly:
494
+ // a REPLY is addressed to the transport id the percept arrived with
495
+ // (`discord:1019…`) while a PROACTIVE message is addressed to the anchor the
496
+ // executive resolved (`ke:…`), and they are the same someone. The PROMPT half
497
+ // resolved that from the start; this half, the one that damps, never did — so
498
+ // having just answered a person did not damp reaching out to them.
499
+ const key = canon ? canon( target ) : target
500
+ if( at > ( out.get( key ) ?? -Infinity ) ) out.set( key, at )
501
+ }
502
+
503
+ return out
504
+ }
505
+
151
506
  // ── ACP-P1: entity correspondence (ACTION_CONDITIONED_PREDICTION §2) ─────────
152
507
 
153
508
  /** Salience multiplier for an entity-correspondence match — gentler than a