@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
@@ -116,6 +116,100 @@ export function traitEmphasis( value: number ): TraitEmphasis | null {
116
116
  // low" yet "above my norm" (low overall, but high for me lately).
117
117
  const TRAIT_NORM_BAND = 0.12 // deviation from personal baseline to read as above/below my norm
118
118
 
119
+ /**
120
+ * One percept, as the mind reads it: the engine's LABEL, and beneath it the
121
+ * EVIDENCE the host actually sent.
122
+ *
123
+ * The whole line is one unit on purpose. Rendering the label and the data
124
+ * separately let a mutation that dropped the data from the call site pass every
125
+ * test — the data renderer stayed correct while the prompt stopped showing it,
126
+ * which is exactly the state this fix exists to leave behind. Same lesson as
127
+ * `temporalLine`.
128
+ */
129
+ export function perceptLine(
130
+ p: { category: string; summary: string; salience: number; data?: unknown },
131
+ ): string {
132
+ return `- [${ p.category }] ${ p.summary } (salience: ${ p.salience.toFixed( 2 ) })${ perceptData( p.data ) }`
133
+ }
134
+
135
+ /**
136
+ * One working-memory item, as the mind reads it — label, then evidence.
137
+ *
138
+ * A sibling of `perceptLine` and a unit for the same reason: rendering the data
139
+ * inline let a mutation that dropped it pass every test. A percept entity is
140
+ * swept after 2 ticks and the executive fires on its own schedule, so MEMORY is
141
+ * often where a mind actually meets an observation — dropping the evidence here
142
+ * loses it just as completely as never storing it, one step later.
143
+ */
144
+ export function ruminationLine(
145
+ w: { type: string; summary: string; activation: number; data?: unknown },
146
+ ): string {
147
+ return `- [${ w.type }] ${ w.summary } (activation: ${ w.activation.toFixed( 2 ) })${ perceptData( w.data ) }`
148
+ }
149
+
150
+ /**
151
+ * A percept's own data, rendered under its label — what the host actually sent.
152
+ *
153
+ * The label is the engine's words about the signal; this is the evidence. A mind
154
+ * that only ever sees labels is being handed conclusions, and the whole job of a
155
+ * mind is to make meaning by connecting pieces of information it can see.
156
+ *
157
+ * Indented on its own line rather than inlined: it can be long, and a host is
158
+ * explicitly not asked to keep it short — what it sent is what it sent.
159
+ */
160
+ function perceptData( data: unknown ): string {
161
+ if( data === undefined || data === null ) return ''
162
+ if( typeof data === 'string') return data.length > 0 ? `\n ${ data }` : ''
163
+
164
+ try {
165
+ // A host's own `summary`, when it offered one, is already the label on the
166
+ // line above. Repeating it underneath is noise, and noise in a percept is
167
+ // not free — it is read every tick the percept is alive. NOT reshaping: the
168
+ // stored data keeps every field, this only declines to print one twice.
169
+ const shown = Array.isArray( data )
170
+ ? data
171
+ : Object.fromEntries( Object.entries( data as Record<string, unknown> ).filter( ( [ k ] ) => k !== 'summary') )
172
+ const json = JSON.stringify( shown )
173
+ return json === '{}' || json === '[]' ? '' : `\n ${ json }`
174
+ }
175
+ catch { return '' }
176
+ }
177
+
178
+ /**
179
+ * What a phase of the cycle is CALLED, in the words a mind would use.
180
+ *
181
+ * NOT the clock. The hour this is computed from is the oscillator's own — free-
182
+ * running from the tick unless a host entrains it — so it is what the BODY
183
+ * reads, which is a different claim from what time it is. A jet-lagged body
184
+ * says night at noon and is not lying; it is reporting itself.
185
+ *
186
+ * The prompt used to render the raw hour beside this, and the two disagreed on
187
+ * every tick of every Will ever run (`Time: 12.0h (night)`). The hour is gone
188
+ * from the prompt entirely now: it is a fact about the world, and a fact about
189
+ * the world is something a mind goes and gets — see the `check-time` schema.
190
+ */
191
+ export function temporalLine( timeOfDay: number, circadian: number ): string {
192
+ return `Body rhythm: it feels like ${ labelForHour( timeOfDay ) } to me`
193
+ + ` (my own cycle, not a clock — I use \`check-time\` to find out the actual hour).`
194
+ + ` Circadian phase: ${ circadian.toFixed( 2 ) }.`
195
+ }
196
+
197
+ /**
198
+ * The label, from the hour. Kept separate from `temporalLine` only so the bands
199
+ * are readable; nothing outside this file should need it.
200
+ */
201
+ export function labelForHour( h: number ): string {
202
+ const hour = ( ( h % 24 ) + 24 ) % 24
203
+ return hour < 2 ? 'deep night'
204
+ : hour < 5 ? 'late night'
205
+ : hour < 8 ? 'early morning'
206
+ : hour < 11 ? 'morning'
207
+ : hour < 14 ? 'midday'
208
+ : hour < 17 ? 'afternoon'
209
+ : hour < 21 ? 'evening'
210
+ : 'night'
211
+ }
212
+
119
213
  export function normEmphasis( value: number, mean: number ): 'above' | 'below' | null {
120
214
  const d = value - mean
121
215
  if( d >= TRAIT_NORM_BAND ) return 'above'
@@ -270,7 +364,15 @@ export interface PromptBuildOptions {
270
364
  * how one mind ends up opening a second conversation with someone it is already
271
365
  * talking to — or telling one person it has contacted another when it has not.
272
366
  */
273
- activeConversations?: { entityId: string; name?: string; sinceTick: number }[]
367
+ activeConversations?: {
368
+ entityId: string
369
+ name?: string
370
+ sinceTick: number
371
+ /** What the mind worked out in that thread — its own reasoning, come back. */
372
+ concluded?: string
373
+ /** Commitments it made there toward someone NOT in the thread. */
374
+ promised?: Array<{ what: string; target?: string; gist?: string; tick: number }>
375
+ }[]
274
376
  }
275
377
 
276
378
  // ── PromptFactory ────────────────────────────────────────────
@@ -616,16 +718,6 @@ completionType guide:
616
718
  const timeOfDay = context.worldState.timeOfDay
617
719
  const threatLevel = context.worldState.threatLevel
618
720
 
619
- // Map raw circadian phase (0–1) to a human-readable label for full temporal awareness.
620
- // 0 = midnight, 0.5 = noon, 1 = midnight.
621
- const phaseLabel = circadian < 0.083 ? 'deep night'
622
- : circadian < 0.208 ? 'late night'
623
- : circadian < 0.333 ? 'early morning'
624
- : circadian < 0.458 ? 'morning'
625
- : circadian < 0.583 ? 'midday'
626
- : circadian < 0.708 ? 'afternoon'
627
- : circadian < 0.833 ? 'evening'
628
- : 'night'
629
721
 
630
722
  const energyGuidance = this._buildEnergyGuidance( energy )
631
723
  const stressGuidance = this._buildStressGuidance( stress )
@@ -676,7 +768,7 @@ completionType guide:
676
768
  Energy: ${energy.toFixed( 1 )}/100
677
769
  Sleep Pressure: ${sleepPressure.toFixed( 1 )}/100
678
770
  Stress: ${stress.toFixed( 1 )}/100${threatLine}
679
- Time: ${timeOfDay.toFixed( 1 )}h (${phaseLabel}, circadian: ${circadian.toFixed( 2 )})
771
+ ${temporalLine( timeOfDay, circadian )}
680
772
  Cognitive capacity:${capacityNote}
681
773
  Epistemic uncertainty: ${( epistemicUncertainty * 100 ).toFixed( 0 )}%${uncertaintyLabel}
682
774
  Tick: ${state.tick}
@@ -721,7 +813,7 @@ Dominance: ${context.affect.dominance.toFixed( 2 )}${context.affect.blends.lengt
721
813
  : ''
722
814
 
723
815
  const perceptsBlock = has('percepts')
724
- ? `## Percepts (What I Notice)\n${context.percepts.slice( 0, 10 ).map( p => `- [${p.category}] ${p.summary} (salience: ${p.salience.toFixed( 2 )})`).join('\n') || 'Nothing notable'}`
816
+ ? `## Percepts (What I Notice)\n${context.percepts.slice( 0, 10 ).map( perceptLine ).join('\n') || 'Nothing notable'}`
725
817
  : ''
726
818
 
727
819
  // Host abilities afforded right now + what each is for. Framed as
@@ -734,7 +826,7 @@ Dominance: ${context.affect.dominance.toFixed( 2 )}${context.affect.blends.lengt
734
826
  : ''
735
827
 
736
828
  const ruminationsBlock = has('ruminations')
737
- ? `## Active Ruminations (retrieved memories & thoughts)\n${context.workingMemory.map( w => `- [${w.type}] ${w.summary} (activation: ${w.activation.toFixed( 2 )})`).join('\n') || 'Nothing actively held in mind'}`
829
+ ? `## Active Ruminations (retrieved memories & thoughts)\n${context.workingMemory.map( ruminationLine ).join('\n') || 'Nothing actively held in mind'}`
738
830
  : ''
739
831
 
740
832
  const memoriesBlock = has('memories')
@@ -787,10 +879,33 @@ Dominance: ${context.affect.dominance.toFixed( 2 )}${context.affect.blends.lengt
787
879
  // can hold several conversations as one situation rather than as N strangers.
788
880
  // Names come from what the mind has actually learned; the id is shown because
789
881
  // that is what a reach-out must be addressed to.
882
+ // WHAT I worked out there, not only WHO I am with.
883
+ //
884
+ // `executive.facet.sync` has always carried the facet's full reasoning and the
885
+ // master discarded it, so the return leg of the loop was empty: my thinking
886
+ // went down to a facet as "What I've Been Turning Over" and came back as a
887
+ // name. A facet is this same mind with a focus — reading back what it worked
888
+ // out is not a report from a subordinate, it is remembering where my attention
889
+ // has been. First person throughout, for that reason.
890
+ //
891
+ // A promise made in one thread to someone NOT in it is stated as the plain
892
+ // fact it is. There is deliberately no instruction about it: whether to keep
893
+ // it, drop it, or make a goal of it is mine to decide, and I have a whole
894
+ // faculty for that.
790
895
  const conversationsBlock = ( options.mode !== 'facet' && options.activeConversations?.length )
791
- ? `## In Conversation Now\n${options.activeConversations.map( c =>
792
- `- ${c.name ?? 'someone'} (id: ${c.entityId})`
793
- ).join('\n')}\nThese threads are already open I am in them. Reaching out to one of these people again starts a second, parallel thread with them.`
896
+ ? `## In Conversation Now\n${options.activeConversations.map( c => {
897
+ const who = `- ${c.name ?? 'someone'} (id: ${c.entityId})`
898
+ const concluded = c.concluded ? `\n What I worked out there: ${c.concluded.trim()}` : ''
899
+ const promised = ( c.promised ?? [] ).map( p =>
900
+ `\n I said there that I would ${p.what}${ p.gist ? ` — about: "${p.gist}"` : '' }`
901
+ + `, at tick ${p.tick}.`
902
+ // Only a CONTACT can be mistaken for already done by having been
903
+ // said, which is the confusion this clause exists to break. A
904
+ // promise about work carries no such ambiguity and gets no lecture.
905
+ + ( p.target ? ' Saying it in that thread did not send it.' : '')
906
+ ).join('')
907
+ return `${who}${concluded}${promised}`
908
+ } ).join('\n')}\nThese threads are already open — I am in them. Reaching out to one of these people again starts a second, parallel thread with them.`
794
909
  : ''
795
910
 
796
911
  // Task focus — what the Will is committed to and the felt cost of switching away.
@@ -997,6 +1112,13 @@ Rest and sleep RESTORE energy. All other actions CONSUME energy. Do not let ener
997
1112
  */
998
1113
 
999
1114
 
1115
+ /**
1116
+ * What the mind has been CHOOSING lately — action types, for the variety
1117
+ * check. Not a history of what happened: nothing here says an act landed.
1118
+ * Retitled from `## Recent Actions` at P3 for exactly that reason (see
1119
+ * `_buildRecentOutcomesSection`) — it sat beside a real history under a name
1120
+ * that claimed to be one.
1121
+ */
1000
1122
  private static _buildActionDiversitySection( recentActionTypes: string[] ): string {
1001
1123
  if( recentActionTypes.length === 0 ) return ''
1002
1124
 
@@ -1006,14 +1128,27 @@ Rest and sleep RESTORE energy. All other actions CONSUME energy. Do not let ener
1006
1128
  ? `\n⚠️ **Action variety alert**: "${recent.filter( t => t === 'reflect' || t === 'observe').join('", "')}" dominated my last ${recent.length} cycles. Choose something DIFFERENT this cycle — e.g. learn, express_emotion, explore, communicate, set_goal, or rest.`
1007
1129
  : ''
1008
1130
 
1009
- return `## Recent Actions (last ${recent.length})
1131
+ return `## What I Have Been Choosing (last ${recent.length})
1010
1132
  ${recent.map( ( t, i ) => `${i + 1}. ${t}`).join(' → ')}${warning}
1011
1133
 
1012
1134
  `
1013
1135
  }
1014
1136
 
1015
1137
  /**
1016
- * Render the recent action outcomes section — closes the Act→Confirm→Perceive loop.
1138
+ * Render what became of what the mind did — closes the Act→Confirm→Sense loop.
1139
+ *
1140
+ * NAMED FOR WHAT IT RENDERS (SIGNAL_BOUNDARY P3). It was `## Recent Action
1141
+ * Outcomes`, which named the `action.outcome` BUS EVENT — a different shape
1142
+ * this section never touches. It renders `action.record` entities: what became
1143
+ * of acts the mind actually took. Meanwhile the section two above it,
1144
+ * `## Recent Actions`, listed action TYPES THE MIND CHOSE — a list of
1145
+ * intentions wearing the name of a history, directly beside a history.
1146
+ *
1147
+ * A mind that cannot tell those two apart cannot tell an intention from an
1148
+ * act, and one live COO could not: asked "have you completed that?", she said
1149
+ * "Yes — it's done", having posted nothing and having no effectors at all. The
1150
+ * section titles are the mind's own labels for its own memory; they are not
1151
+ * decoration.
1017
1152
  * Shows the executive what it tried, whether it landed, and if anything timed out.
1018
1153
  * Only rendered when there are status-bearing action records in state.
1019
1154
  */
@@ -1114,6 +1249,9 @@ ${recent.map( ( t, i ) => `${i + 1}. ${t}`).join(' → ')}${warning}
1114
1249
  const STATUS_BADGE: Record<string, string> = {
1115
1250
  completed: '✓',
1116
1251
  failed: '✗',
1252
+ // Not a failure. I formed it and chose not to complete it — reading that
1253
+ // back as ✗ is how a mind learns it is bad at something it decided against.
1254
+ withheld: '⊘ chose not to',
1117
1255
  awaiting_host: '⏳',
1118
1256
  timed_out: '⏱ TIMED OUT',
1119
1257
  }
@@ -1126,12 +1264,19 @@ ${recent.map( ( t, i ) => `${i + 1}. ${t}`).join(' → ')}${warning}
1126
1264
  return `- ${badge} **${a.type}** (tick ${a.tick}, ${age} ticks ago${planCtx})${outcome}`
1127
1265
  } )
1128
1266
 
1129
- const hasTimeout = recentActions.some( a => a.status === 'timed_out')
1130
- const timeoutNote = hasTimeout
1131
- ? '\n⚠️ **One or more actions timed out** my body dispatched them but received no confirmation. Check if the external handler is working, or choose a different approach.'
1267
+ // A note that can actually fire. The one here before keyed on a `timed_out`
1268
+ // status nothing produces — and it sat inside a section that had never
1269
+ // rendered at all, so neither could ever have been seen.
1270
+ const failed = recentActions.filter( a => a.status === 'failed').length
1271
+ const didNotLand = failed > 0
1272
+ ? `\n⚠️ **${ failed } of these did not land** — my body attempted them and they did not complete.`
1132
1273
  : ''
1274
+ // Invariant, and identical on every branch: this is the sentence that makes
1275
+ // the section load-bearing rather than decorative, and a mind must not get a
1276
+ // differently-worded version of it depending on how its week went.
1277
+ const note = `${ didNotLand }\nThis is what I HAVE done, not what I meant to do. If something I intended is not on this list, it did not happen.`
1133
1278
 
1134
- return `## Recent Action Outcomes\n${lines.join('\n')}${timeoutNote}\n\n`
1279
+ return `## What Became Of What I Did\n${lines.join('\n')}${note}\n\n`
1135
1280
  }
1136
1281
 
1137
1282
  /**
@@ -96,15 +96,6 @@ export interface ExecutiveOutputFull {
96
96
  * Present ⇒ nothing is sent, whatever else the response contains.
97
97
  */
98
98
  noMessage?: string
99
- /**
100
- * @deprecated Legacy JSON reply format — no longer emitted by conversation facets.
101
- * Kept for backward compatibility with any tests/tooling that inspect parsed output.
102
- */
103
- conversationReplies?: Array<{
104
- targetEntityId: string
105
- targetEntityName: string
106
- messages: string[]
107
- }>
108
99
  /**
109
100
  * System 2 only — the distinct approaches the master generated and weighed before
110
101
  * committing, retained for explainability/auditability (and a future regret /
@@ -254,8 +245,11 @@ export interface ExecutiveContext {
254
245
  relevantPlanIds: string[]
255
246
  percepts: Array<{
256
247
  category: string
248
+ /** The engine's label for the signal. */
257
249
  summary: string
258
250
  salience: number
251
+ /** What the host actually sent, whole — the evidence under the label. */
252
+ data?: unknown
259
253
  }>
260
254
  /**
261
255
  * Host-declared abilities afforded to the Will *right now* — what it can do in
@@ -272,8 +266,11 @@ export interface ExecutiveContext {
272
266
  }>
273
267
  workingMemory: Array<{
274
268
  type: string
269
+ /** The engine's label for the item. */
275
270
  summary: string
276
271
  activation: number
272
+ /** What a host sent, whole — carried through memory, not only into it. */
273
+ data?: unknown
277
274
  }>
278
275
  memories: Array<{
279
276
  content: string
@@ -290,15 +287,21 @@ export interface ExecutiveContext {
290
287
  /** How many beliefs exist but were not included due to the relevance cap. */
291
288
  beliefsOmitted: number
292
289
  /**
293
- * Recent action outcomes — shows the executive what it already tried and whether it
294
- * landed. Built from `decision.record` entities with an `actionStatus` set.
295
- * Surfaces the Act→Confirm→Perceive feedback loop into the executive's reasoning.
290
+ * What became of what it did the Act→Confirm→Perceive loop, surfaced.
291
+ *
292
+ * Built from `action.record` entities the executive writes from the
293
+ * `action.outcome` / `action.withheld` events it already receives. It used to
294
+ * be built from `decision.record` entities carrying an `actionStatus`, a field
295
+ * read in one place and written in none — so this was empty in every prompt a
296
+ * live mind ever received, and it could see what it had SAID but never what it
297
+ * had DONE. See `action.record.ts`.
296
298
  */
297
299
  recentActions: Array<{
298
300
  /** Effector name that was invoked */
299
301
  type: string
300
- /** Lifecycle status set by ActionExecutor */
301
- status: 'completed' | 'failed' | 'awaiting_host' | 'timed_out'
302
+ /** How it resolved. `withheld` is distinct from `failed` on purpose: the
303
+ * mind formed the act and chose not to complete it. */
304
+ status: 'completed' | 'failed' | 'withheld'
302
305
  /** Tick the action was executed or dispatched */
303
306
  tick: number
304
307
  /** Short outcome description — truncated to 120 chars */
@@ -312,7 +315,7 @@ export interface ExecutiveContext {
312
315
  * The one thing it could never see about itself. `conversation.sent` has been in
313
316
  * state since the beginning — 57 records on the Will this was found on — and
314
317
  * reached no prompt at all, so the sole evidence of having spoken was a `✓
315
- * reach-out` line under Recent Action Outcomes: no words, no person, and a tick
318
+ * reach-out` line under What Became Of What I Did: no words, no person, and a tick
316
319
  * mark asserting it had worked. That is why the same question went out eleven
317
320
  * times in two and a half minutes; from the inside each one was the first.
318
321
  *
@@ -24,18 +24,18 @@ import type {
24
24
  SimulationContext,
25
25
  ReadonlySimulationState,
26
26
  StateCommands,
27
- SimulationEvent,
28
27
  SimulationEntity,
29
28
  } from '#core/types'
30
29
  import type { SimulationEngine, EngineResult, CognitiveEngine } from '#cognition/types'
31
30
  import type { CognitiveEventSchema } from '#cognition/schema.registry'
32
31
  import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
32
+ import { perceptEntity, PERCEPT_SUMMARY_CAP } from '#cognition/percept.entity'
33
33
  import { GenerativeModel } from '#cognition/generative.model'
34
34
  import {
35
- CONSEQUENCE_TYPE, ATTENUATION, CORRESPONDENCE_ATTENUATION,
35
+ ATTENUATION, CORRESPONDENCE_ATTENUATION,
36
36
  liveConsequences, matchConsequenceText, matchConsequenceEntity,
37
37
  } from '#agency/consequence'
38
- import { REVOCATION_TYPE } from '#agency/revocation'
38
+ import { MIND_OWN_ENTITY_TYPES } from '#cognition/sense.boundary'
39
39
 
40
40
  export interface ExteroceptionConfig {
41
41
  /** Maximum percepts to produce per tick */
@@ -43,9 +43,18 @@ export interface ExteroceptionConfig {
43
43
  /** Default salience for unmarked percepts */
44
44
  defaultSalience?: number
45
45
  /** Whether to emit percept events */
46
- emitPerceptEvents?: boolean
47
46
  /** Entity types to always treat as high-salience */
48
47
  highPriorityTypes?: string[]
48
+ /**
49
+ * Where this mind ends — the entity types its own engines write about its own
50
+ * operation, which it therefore cannot encounter as world events. Live, so a
51
+ * host adding a cognitive engine after assembly is accounted for.
52
+ *
53
+ * Unwired (a bare harness, a unit test) it falls back to the shipped anatomy,
54
+ * which is right for every assembly that adds no cognitive engine of its own.
55
+ * See `#cognition/sense.boundary`.
56
+ */
57
+ endogenous?: () => ReadonlySet<string>
49
58
  bus?: CognitiveBus
50
59
  }
51
60
 
@@ -63,51 +72,60 @@ interface RawPercept {
63
72
  valenceSource?: 'entity' | 'ambient'
64
73
  }
65
74
 
66
- // Skip internal entities — they're not external percepts.
67
- // Percepts about percepts create a feedback loop that floods
68
- // working memory and episodic storage with noise.
69
- const internalTypes = new Set([
70
- 'percept', 'percept.social', 'working_memory.item',
71
- 'interoception', 'attention.focus', 'decision.record',
72
- 'task.focus', 'self_observation',
73
- 'goal', 'belief', 'plan', 'narrative_chapter',
74
- 'introspection', 'self_narrative', 'cognitive_bias',
75
- 'effector.created', 'empathic_state', 'attachment.bond',
76
- 'theory_of_mind', 'reputation', 'episodic_memory',
77
- // Internal state entities written by our own engines — not external events
78
- 'affect.blends', 'executive.summary',
79
- // The agency's own forward-model records (EXAFFERENCE P1/P2): perceiving our
80
- // expected-consequence descriptors would be a self-noise loop.
81
- CONSEQUENCE_TYPE,
82
- // The agency's own revocation tombstones (EXAFFERENCE P4) — internal bookkeeping.
83
- REVOCATION_TYPE,
84
- ])
85
-
86
75
  export class Exteroception implements SimulationEngine, CognitiveEngine {
87
76
  readonly name = 'exteroception'
88
77
 
89
78
  private _maxPerceptsPerTick: number
90
79
  private _defaultSalience: number
91
- private _emitPerceptEvents: boolean
92
80
  private _highPriorityTypes: Set<string>
93
- private _previousEntityVersions = new Map<string, number>() // entityId → updatedAt
81
+ /**
82
+ * entityId → what was last seen of it. The TYPE is remembered alongside the
83
+ * version because a removal has to answer the same question an appearance
84
+ * does — "was this mine?" — and by then the entity is gone. It used to be
85
+ * answered by a second, separately-drifting list of id prefixes.
86
+ */
87
+ private _previousEntityVersions = new Map<string, { at: Timestamp; type: string }>()
94
88
 
95
89
  private _bus: CognitiveBus | null = null
96
90
 
97
91
  private readonly _model = new GenerativeModel()
98
92
 
93
+ /** Live sense boundary + a memo, so a 50-engine union isn't rebuilt per tick. */
94
+ private _boundary: ( () => ReadonlySet<string> ) | null
95
+ private _endogenousMemo: ReadonlySet<string> = MIND_OWN_ENTITY_TYPES
96
+
99
97
 
100
98
  constructor( config: ExteroceptionConfig = {} ){
101
99
  this._bus = config.bus ?? null
100
+ this._boundary = config.endogenous ?? null
102
101
  this._maxPerceptsPerTick = config.maxPerceptsPerTick ?? 50
103
102
  this._defaultSalience = config.defaultSalience ?? 0.3
104
- this._emitPerceptEvents = config.emitPerceptEvents ?? true
105
103
  this._highPriorityTypes = new Set( config.highPriorityTypes ?? [
106
104
  'message', 'notification', 'alert', 'threat', 'goal',
107
105
  ])
108
106
  }
109
107
  attachBus( bus: CognitiveBus ): void { this._bus = bus }
110
108
 
109
+ /**
110
+ * Wire the live sense boundary. Separate from construction because the
111
+ * boundary is derived from the assembled engine list, and this sense is
112
+ * constructed before that list exists (same reason `attachBus` exists).
113
+ */
114
+ attachBoundary( resolve: () => ReadonlySet<string> ): void { this._boundary = resolve }
115
+
116
+ /**
117
+ * The types that are THIS mind rather than its world.
118
+ *
119
+ * Re-read each tick so a host engine registered after assembly still lands
120
+ * inside the boundary; the memo makes that a set-identity check in the common
121
+ * case, since `endogenousTypes` returns the shipped set unchanged when nothing
122
+ * extra is declared.
123
+ */
124
+ private _endogenous(): ReadonlySet<string> {
125
+ if( this._boundary ) this._endogenousMemo = this._boundary()
126
+ return this._endogenousMemo
127
+ }
128
+
111
129
  // ── Engine interface ─────────────────────────────────────
112
130
 
113
131
  subscribes(): string[] { return ['executive.prediction.formed'] }
@@ -132,9 +150,7 @@ export class Exteroception implements SimulationEngine, CognitiveEngine {
132
150
  state: ReadonlySimulationState,
133
151
  context: SimulationContext
134
152
  ): Promise<EngineResult> {
135
- const
136
- events: Array<Omit<SimulationEvent, 'id' | 'timestamp' | 'tick'>> = [],
137
- commands: StateCommands = { set: [], delete: [], metrics: [] }
153
+ const commands: StateCommands = { set: [], delete: [], metrics: [] }
138
154
 
139
155
  const rawPercepts = this._scanWorld( state )
140
156
 
@@ -168,37 +184,27 @@ export class Exteroception implements SimulationEngine, CognitiveEngine {
168
184
  : entityHit ? rp.salience * CORRESPONDENCE_ATTENUATION
169
185
  : rp.salience
170
186
 
171
- const perceptEntity = {
172
- id: `percept-${tick}-${i}`,
173
- type: 'percept',
174
- metadata: {
175
- entityId: rp.entityId,
176
- changeType: rp.changeType,
177
- salience,
178
- category: rp.category,
179
- summary: rp.summary,
180
- provenance: hit ? 'reafferent' : 'exafferent',
181
- ...( hit ? { sourceIntentId: hit.intentId } : {} ),
182
- // affect→percept seam (registry #5): what this percept FEELS like
183
- ...( rp.valence !== undefined ? { valence: rp.valence, valenceSource: rp.valenceSource } : {} ),
184
- tick,
185
- },
186
- }
187
-
188
- commands.set!.push( perceptEntity )
189
-
190
- // Emit percept event for downstream engines
191
- if( this._emitPerceptEvents )
192
- events.push({
193
- type: `percept.${rp.changeType}.${rp.category}`,
194
- source: this.name,
195
- payload: {
196
- entityId: rp.entityId,
197
- salience,
198
- category: rp.category,
199
- summary: rp.summary,
200
- },
201
- })
187
+ // Through the shared constructor (SIGNAL_BOUNDARY P0) rather than a
188
+ // hand-rolled literal. A no-op here — this writer already set every core
189
+ // field — which is exactly why it is the one that proves the shape: four
190
+ // other writers reach it having each forgotten a different one.
191
+ //
192
+ // `provenance` is INFERRED here, legitimately: a match against our own
193
+ // live consequence descriptors is the efference copy doing its job. The
194
+ // sense door asserts instead, because it has nothing to match against.
195
+ commands.set!.push( perceptEntity( {
196
+ id: `percept-${tick}-${i}`,
197
+ tick,
198
+ salience,
199
+ category: rp.category,
200
+ summary: rp.summary,
201
+ provenance: hit ? 'reafferent' : 'exafferent',
202
+ entityId: rp.entityId,
203
+ changeType: rp.changeType,
204
+ ...( hit ? { sourceIntentId: hit.intentId } : {} ),
205
+ // affect→percept seam (registry #5): what this percept FEELS like
206
+ ...( rp.valence !== undefined ? { valence: rp.valence, valenceSource: rp.valenceSource } : {} ),
207
+ } ) )
202
208
  }
203
209
 
204
210
  // Clean up old percepts from previous ticks (keep last 2 ticks of percepts)
@@ -211,13 +217,12 @@ export class Exteroception implements SimulationEngine, CognitiveEngine {
211
217
  )
212
218
 
213
219
 
214
- // Phase C + F: publish cognitive event gated by prediction error
220
+ // The percept RATE is still learned`percept.rate` carries a precision set
221
+ // from `executive.prediction.formed`, and the observation is what keeps that
222
+ // baseline honest. What is gone is the `percept.batch.ingested` event it used
223
+ // to publish: one publisher, zero subscribers, ever.
215
224
  const _bus = this._bus
216
- if( _bus && capped.length > 0 ){
217
- const predErr = this._model.observe('percept.rate', capped.length )
218
- if( !predErr.gated )
219
- _bus.publish({ type: 'percept.batch.ingested', version: 1, sourceEngine: this.name, salience: Math.max( 0.2, predErr.salience ), payload: { count: capped.length } })
220
- }
225
+ if( capped.length > 0 ) this._model.observe('percept.rate', capped.length )
221
226
 
222
227
  // percept.category.updated — one event per distinct category so subscribers
223
228
  // (aesthetic.evaluator etc.) get a structured signal without scanning percepts
@@ -229,7 +234,7 @@ export class Exteroception implements SimulationEngine, CognitiveEngine {
229
234
  _bus.publish({ type: 'percept.category.updated', version: 1, sourceEngine: this.name, salience: Math.min( 1, count * 0.15 + 0.2 ), payload: { category, count } })
230
235
  }
231
236
 
232
- return { events: events.length > 0 ? events : undefined, commands }
237
+ return { commands }
233
238
  }
234
239
 
235
240
  // ── Internal ─────────────────────────────────────────────
@@ -243,18 +248,19 @@ export class Exteroception implements SimulationEngine, CognitiveEngine {
243
248
  private _scanWorld( state: ReadonlySimulationState ): RawPercept[] {
244
249
  const percepts: RawPercept[] = []
245
250
  const currentIds = new Set<string>()
251
+ const mine = this._endogenous()
246
252
 
247
253
  for( const [ id, entity ] of state.entities ){
248
254
  currentIds.add( id )
249
255
 
250
- if( internalTypes.has( entity.type ) ){
251
- // Still track the version so we don't get "appeared" events
252
- // when these internal entities are first created
253
- this._previousEntityVersions.set( id, entity.updatedAt )
256
+ if( mine.has( entity.type ) ){
257
+ // Still track it, so it never reads as "appeared" if the boundary later
258
+ // widens but it is machinery, not world, and is not perceived.
259
+ this._previousEntityVersions.set( id, { at: entity.updatedAt, type: entity.type } )
254
260
  continue
255
261
  }
256
262
 
257
- const previousVersion = this._previousEntityVersions.get( id )
263
+ const previousVersion = this._previousEntityVersions.get( id )?.at
258
264
 
259
265
  if( previousVersion === undefined ){
260
266
  percepts.push({
@@ -279,32 +285,27 @@ private _scanWorld( state: ReadonlySimulationState ): RawPercept[] {
279
285
  })
280
286
  }
281
287
 
282
- this._previousEntityVersions.set( id, entity.updatedAt )
288
+ this._previousEntityVersions.set( id, { at: entity.updatedAt, type: entity.type } )
283
289
  }
284
290
 
285
- // Detect removed entities (not internal types)
286
- for( const [ id ] of this._previousEntityVersions ){
287
- if( !currentIds.has( id ) ){
288
- // Only report removal of meaningful entities
289
- // We can't check the type here since the entity is gone,
290
- // but we can infer from the ID prefix
291
- if( !id.startsWith('percept-')
292
- && !id.startsWith('wm-')
293
- && !id.startsWith('attention-')
294
- && !id.startsWith('decision-')
295
- && !id.startsWith('interoception-')
296
- && !id.startsWith('task-')
297
- && !id.startsWith('self-obs-') ){
298
- percepts.push({
299
- entityId: id,
300
- changeType: 'removed',
301
- salience: 0.4,
302
- category: 'removed',
303
- summary: `Entity removed: ${id}`,
304
- })
305
- }
306
- this._previousEntityVersions.delete( id )
307
- }
291
+ // Something that was there is gone. The same question as an appearance —
292
+ // was it mine or the world's? — and answerable now because the remembered
293
+ // type outlives the entity. It used to be guessed from a handful of id
294
+ // prefixes, which is why 17,802 of one quiet run's 36,721 percepts were the
295
+ // mind watching its own affordance field be torn down each tick.
296
+ for( const [ id, seen ] of this._previousEntityVersions ){
297
+ if( currentIds.has( id ) ) continue
298
+
299
+ if( !mine.has( seen.type ) )
300
+ percepts.push({
301
+ entityId: id,
302
+ changeType: 'removed',
303
+ salience: 0.4,
304
+ category: 'removed',
305
+ summary: `Entity removed: ${id}`,
306
+ })
307
+
308
+ this._previousEntityVersions.delete( id )
308
309
  }
309
310
 
310
311
  percepts.sort( ( a, b ) => b.salience - a.salience )
@@ -374,7 +375,7 @@ private _scanWorld( state: ReadonlySimulationState ): RawPercept[] {
374
375
  ? `${name} appears` : `${name} changed`
375
376
  }
376
377
  if( description ){
377
- return description.slice( 0, 100 )
378
+ return description.slice( 0, PERCEPT_SUMMARY_CAP )
378
379
  }
379
380
 
380
381
  return `New ${entity.type}: ${entity.id.slice(0, 30)}`