@mindot/will 0.9.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 (110) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +16660 -15206
  4. package/dist/index.js.map +1 -1
  5. package/dist/{channels → surface/channels}/discord.d.ts +50 -3
  6. package/dist/{channels → surface/channels}/discord.js +102 -7
  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 +7 -4
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +4406 -2904
  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-DbDj_TEH.d.ts → will-evj9_vrd.d.ts} +5699 -4854
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +237 -3
  19. package/src/cognition/agency/conversation.aim.ts +32 -0
  20. package/src/cognition/agency/engines/action.selector.ts +59 -3
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +114 -10
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +362 -27
  24. package/src/cognition/agency/engines/reafference.engine.ts +43 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/reconcile.learning.ts +5 -4
  27. package/src/cognition/agency/schemas/innate.ts +96 -1
  28. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  29. package/src/cognition/agency/selection.scoring.ts +16 -0
  30. package/src/cognition/agency/settlement.ts +203 -0
  31. package/src/cognition/agency/types.ts +38 -1
  32. package/src/cognition/config.mirror.entities.ts +0 -1
  33. package/src/cognition/event.schemas.ts +0 -8
  34. package/src/cognition/faculties/affective.blender.ts +21 -4
  35. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  36. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  37. package/src/cognition/faculties/executive.engine/context.ts +41 -35
  38. package/src/cognition/faculties/executive.engine/engine.ts +270 -113
  39. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +126 -59
  40. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +10 -0
  41. package/src/cognition/faculties/executive.engine/parser.ts +100 -38
  42. package/src/cognition/faculties/executive.engine/prompt.factory.ts +168 -23
  43. package/src/cognition/faculties/executive.engine/types.ts +18 -15
  44. package/src/cognition/faculties/exteroception.ts +99 -98
  45. package/src/cognition/faculties/goal.manager.ts +50 -2
  46. package/src/cognition/faculties/known.entity.tracker.ts +22 -3
  47. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  48. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  49. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  50. package/src/cognition/faculties/working.memory.ts +6 -1
  51. package/src/cognition/instruction.handler.ts +1 -1
  52. package/src/cognition/memory/vector.embedder.ts +1 -3
  53. package/src/cognition/percept.entity.ts +122 -0
  54. package/src/cognition/sense.boundary.ts +176 -0
  55. package/src/cognition/senses/audition.engine/engine.ts +97 -16
  56. package/src/cognition/senses/base.sense.engine.ts +104 -6
  57. package/src/cognition/senses/index.ts +71 -13
  58. package/src/cognition/senses/provenance.ts +128 -0
  59. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  60. package/src/cognition/social.identity.ts +23 -0
  61. package/src/cognition/utilities/token.tracker.ts +36 -5
  62. package/src/core/orchestrator.ts +13 -0
  63. package/src/index.ts +39 -5
  64. package/src/llm/gate.ts +48 -0
  65. package/src/llm/index.ts +102 -25
  66. package/src/llm/wire.contracts.ts +38 -0
  67. package/src/pma/index.ts +1 -1
  68. package/src/stem/index.ts +92 -21
  69. package/src/stem/mind.ts +26 -2
  70. package/src/stem/policy/arbiter.ts +10 -0
  71. package/src/stem/profiles/built-in.ts +7 -0
  72. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  73. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  74. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  75. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  76. package/src/{profiles → stem/profiles}/index.ts +1 -1
  77. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  78. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  79. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  80. package/src/stem/tracts/effector/types.ts +110 -0
  81. package/src/stem/tracts/effector.controller.ts +112 -401
  82. package/src/stem/tracts/outbox.controller.ts +30 -17
  83. package/src/stem/tracts/sensory.controller.ts +6 -6
  84. package/src/stem/tracts/session.logger.ts +6 -1
  85. package/src/stem/tracts/transport/types.ts +20 -1
  86. package/src/stem/tracts/transport.controller.ts +33 -9
  87. package/src/{channels → surface/channels}/discord.ts +224 -7
  88. package/src/{channels → surface/channels}/roster.ts +1 -1
  89. package/src/{channels → surface/channels}/types.ts +1 -1
  90. package/src/{channels → surface/channels}/whatsapp.ts +9 -6
  91. package/src/{cli.ts → surface/cli.ts} +6 -6
  92. package/src/{host → surface/host}/boot.ts +3 -3
  93. package/src/{host → surface/host}/utterances.ts +2 -2
  94. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  95. package/src/{mcp → surface/mcp}/server.ts +19 -5
  96. package/src/{sdk → surface/sdk}/will.ts +156 -28
  97. package/src/{serve → surface/serve}/server.ts +11 -4
  98. package/src/types.ts +24 -3
  99. package/dist/channels/discord.js.map +0 -1
  100. package/dist/channels/whatsapp.js.map +0 -1
  101. package/dist/cli.js.map +0 -1
  102. package/dist/mcp/effectors.js.map +0 -1
  103. package/src/extensions/livestream.ext.ts +0 -570
  104. package/src/extensions/time.ext.ts +0 -339
  105. package/src/profiles/built-in.ts +0 -7
  106. package/src/runners/coherence.runner.ts +0 -49
  107. package/src/runners/outreach.runner.ts +0 -155
  108. package/src/runners/social.runner.ts +0 -179
  109. package/src/runners/thin-shim.runner.ts +0 -205
  110. /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.9.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,6 +214,15 @@ 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 ){
@@ -132,7 +238,12 @@ export function liveConsequences(
132
238
  // reach-out to them and delivered nothing, and the P2 matcher would equally
133
239
  // have attenuated their genuine replies as its own echo.
134
240
  if( d.tick > tick ) continue
135
- if( tick < d.expiresAt ) out.push( d )
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 )
136
247
  }
137
248
  return out.sort( ( a, b ) => ( a.intentId < b.intentId ? -1 : a.intentId > b.intentId ? 1 : 0 ) )
138
249
  }
@@ -204,20 +315,127 @@ export function enactionFootprint(
204
315
  * that speaking happens to be schematised.
205
316
  */
206
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>,
207
375
  ): number {
208
- if( !targetEntityId ) return 0
209
376
  if( windowTicks <= 0 ) return 0
210
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
+
211
413
  let strongest = 0
212
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
+
213
421
  const spoken = spokenAt?.get( targetEntityId )
214
422
  if( spoken !== undefined ){
215
423
  const remaining = ( windowTicks - ( tick - spoken ) ) / windowTicks
216
424
  if( remaining > strongest ) strongest = remaining
217
425
  }
218
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
+
219
435
  for( const d of descriptors ){
220
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
221
439
  // Measured from when the act happened, against the SATIATION window — not from
222
440
  // the descriptor's own expiry, which is the echo window and a different
223
441
  // question. Conflating them meant "how long before I say this again" was
@@ -242,6 +460,15 @@ export function enactionFootprint(
242
460
  */
243
461
  export function spokenAtByEntity(
244
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,
245
472
  ): Map<string, number> {
246
473
  const out = new Map<string, number>()
247
474
 
@@ -263,7 +490,14 @@ export function spokenAtByEntity(
263
490
  : typeof e.updatedAtTick === 'number' ? e.updatedAtTick
264
491
  : typeof e.tick === 'number' ? e.tick
265
492
  : 0
266
- if( at > ( out.get( target ) ?? -Infinity ) ) out.set( target, at )
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 )
267
501
  }
268
502
 
269
503
  return out
@@ -187,6 +187,38 @@ export function lastHeardByEntity( entities: ReadonlyMap<string, EntityLike> ):
187
187
  /** When someone last spoke to us, AND what they said. */
188
188
  export interface Heard { tick: Tick; preview: string }
189
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
+
190
222
  /** A turn still in the air: said, not acknowledged-only, and not yet answered. */
191
223
  export function isOpen( t: SpokenTurn ): boolean {
192
224
  return !t.isAck && t.answeredAt === undefined
@@ -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 ],
@@ -544,7 +562,7 @@ export class ActionSelector implements CognitiveEngine {
544
562
  // happens: an intention that has been acted on is no longer an intention, and
545
563
  // nothing was deleting these. See MotorSchemaExecutor._dischargeWill.
546
564
  ...( winner.affordance.evokedBy ? { evokedBy: winner.affordance.evokedBy } : {} ),
547
- // Plan provenance (when a plan's frontier-step prior won the competition) —
565
+ // The plan link (when a plan's frontier-step prior won the competition) —
548
566
  // flows through the executor's action.outcome so the PlanningEngine advances.
549
567
  ...( winner.affordance.planId ? { planId: winner.affordance.planId } : {} ),
550
568
  ...( winner.affordance.stepId ? { stepId: winner.affordance.stepId } : {} ),
@@ -628,7 +646,9 @@ export class ActionSelector implements CognitiveEngine {
628
646
  set: compositeTombstone
629
647
  ? [ intent, revocationEntity( compositeTombstone, compositeFrom ?? '', rupture, tick ) ]
630
648
  : [ intent ],
631
- ...( preemptDelete ? { delete: [ preemptDelete ] } : {} ),
649
+ ...( preemptDelete || settlementDrops.length > 0
650
+ ? { delete: [ ...( preemptDelete ? [ preemptDelete ] : [] ), ...settlementDrops ] }
651
+ : {} ),
632
652
  metrics: [
633
653
  [ 'agency.field.eligible', eligible.length ],
634
654
  [ 'agency.selection.busy', 0 ],
@@ -685,6 +705,14 @@ function computeRupture(
685
705
  for( const e of state.entities.values() ){
686
706
  if( e.type !== 'percept') continue
687
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.
688
716
  if( str( m?.['provenance'] ) !== 'exafferent') continue
689
717
  const pTick = num( m?.['tick'], -1 )
690
718
  if( pTick < 0 || tick - pTick > RUPTURE_WINDOW_TICKS ) continue
@@ -792,7 +820,31 @@ function buildBias( state: ReadonlySimulationState ): BiasContext {
792
820
 
793
821
  // ─── entity decoding ─────────────────────────────────────────────────────────
794
822
 
795
- 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 {
796
848
  const meta = ( m ?? {} ) as Record<string, unknown>
797
849
  return {
798
850
  id,
@@ -810,6 +862,10 @@ function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<st
810
862
  planBias: typeof meta['planBias'] === 'number' ? ( meta['planBias'] as number ) : undefined,
811
863
  willBias: typeof meta['willBias'] === 'number' ? ( meta['willBias'] as number ) : undefined,
812
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'] ),
813
869
  planId: str( meta['planId'] ),
814
870
  stepId: str( meta['stepId'] ),
815
871
  tick: num( meta['tick'], 0 ),