@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
@@ -59,6 +59,18 @@ export interface ExecutiveOutputFull {
59
59
  name?: string
60
60
  learned?: string[]
61
61
  feeling?: number
62
+ /**
63
+ * "This is the same someone as that" — another keid I now believe is this
64
+ * same referent, fusing two of my records into one.
65
+ *
66
+ * The mind's own verdict on an identity, and it may do what the recognition
67
+ * heuristic will not: absorb an ESTABLISHED relationship. The heuristic is
68
+ * right to refuse — fusing two real people who share a name would take one of
69
+ * them's whole history — but the mind has evidence a name-match does not,
70
+ * usually because somebody just told it. Without this, the same human
71
+ * well-established on two channels stayed two people permanently.
72
+ */
73
+ sameAs?: string
62
74
  }>
63
75
  newGoals?: Array<{
64
76
  description: string
@@ -70,6 +82,8 @@ export interface ExecutiveOutputFull {
70
82
  goalsToAbandon?: Array<{ goalId: string; reason: string }>
71
83
  goalsToReprioritize?: Array<{ goalId: string; newPriority: number; reason: string }>
72
84
  selfObservations?: string[]
85
+ /** Compound actions the mind is naming as single skills (see ProposedSkill). */
86
+ newSkills?: ProposedSkill[]
73
87
  /**
74
88
  * Plain-text reply from a conversation facet — populated by parseResponse()
75
89
  * from the [REPLY_TEXT]...[/REPLY_TEXT] block.
@@ -78,14 +92,10 @@ export interface ExecutiveOutputFull {
78
92
  */
79
93
  replyText?: string
80
94
  /**
81
- * @deprecated Legacy JSON reply format no longer emitted by conversation facets.
82
- * Kept for backward compatibility with any tests/tooling that inspect parsed output.
95
+ * Set when the facet declared it is NOT speaking this cycle, carrying why.
96
+ * Present nothing is sent, whatever else the response contains.
83
97
  */
84
- conversationReplies?: Array<{
85
- targetEntityId: string
86
- targetEntityName: string
87
- messages: string[]
88
- }>
98
+ noMessage?: string
89
99
  /**
90
100
  * System 2 only — the distinct approaches the master generated and weighed before
91
101
  * committing, retained for explainability/auditability (and a future regret /
@@ -235,8 +245,11 @@ export interface ExecutiveContext {
235
245
  relevantPlanIds: string[]
236
246
  percepts: Array<{
237
247
  category: string
248
+ /** The engine's label for the signal. */
238
249
  summary: string
239
250
  salience: number
251
+ /** What the host actually sent, whole — the evidence under the label. */
252
+ data?: unknown
240
253
  }>
241
254
  /**
242
255
  * Host-declared abilities afforded to the Will *right now* — what it can do in
@@ -253,8 +266,11 @@ export interface ExecutiveContext {
253
266
  }>
254
267
  workingMemory: Array<{
255
268
  type: string
269
+ /** The engine's label for the item. */
256
270
  summary: string
257
271
  activation: number
272
+ /** What a host sent, whole — carried through memory, not only into it. */
273
+ data?: unknown
258
274
  }>
259
275
  memories: Array<{
260
276
  content: string
@@ -271,15 +287,21 @@ export interface ExecutiveContext {
271
287
  /** How many beliefs exist but were not included due to the relevance cap. */
272
288
  beliefsOmitted: number
273
289
  /**
274
- * Recent action outcomes — shows the executive what it already tried and whether it
275
- * landed. Built from `decision.record` entities with an `actionStatus` set.
276
- * 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`.
277
298
  */
278
299
  recentActions: Array<{
279
300
  /** Effector name that was invoked */
280
301
  type: string
281
- /** Lifecycle status set by ActionExecutor */
282
- 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'
283
305
  /** Tick the action was executed or dispatched */
284
306
  tick: number
285
307
  /** Short outcome description — truncated to 120 chars */
@@ -287,6 +309,33 @@ export interface ExecutiveContext {
287
309
  /** planId if this action came from a plan step */
288
310
  planId?: string
289
311
  }>
312
+ /**
313
+ * What the mind has said to people lately, and who has answered.
314
+ *
315
+ * The one thing it could never see about itself. `conversation.sent` has been in
316
+ * state since the beginning — 57 records on the Will this was found on — and
317
+ * reached no prompt at all, so the sole evidence of having spoken was a `✓
318
+ * reach-out` line under What Became Of What I Did: no words, no person, and a tick
319
+ * mark asserting it had worked. That is why the same question went out eleven
320
+ * times in two and a half minutes; from the inside each one was the first.
321
+ *
322
+ * Newest first. Answered turns are kept alongside open ones deliberately — "I
323
+ * asked and they replied" and "I asked and heard nothing" only mean anything
324
+ * against each other.
325
+ */
326
+ spokenTurns: Array<{
327
+ /** Who it was said to, by name where the mind knows one. */
328
+ target: string
329
+ /** The opening words — enough to recognise a thing already said. */
330
+ preview: string
331
+ /** Ticks since it was said. */
332
+ age: number
333
+ /** Unset while still in the air; the mind is told which. */
334
+ answered: boolean
335
+ /** What they said back, when they did. The fact of an answer without its
336
+ * content is worse than silence — it invites acting as though it is known. */
337
+ answeredWith?: string
338
+ }>
290
339
  /** Behavioral disposition loaded from PMA at session start — stable per session. */
291
340
  behavioralDisposition?: {
292
341
  riskTolerance: number
@@ -309,6 +358,10 @@ export interface ExecutiveContext {
309
358
  * Omitted when the Will knows no one.
310
359
  */
311
360
  knownEntities?: Array<{
361
+ /** Where this referent is reachable, and how each place has gone. */
362
+ handles?: Array<{ keid: string; kind: string; answeredAgo?: number }>
363
+ /** Names of referents this one may be the same as — an unsettled identity. */
364
+ mayBeSameAs?: string[]
312
365
  keid: string
313
366
  kind: 'sentient' | 'thing'
314
367
  name?: string
@@ -336,6 +389,25 @@ export interface ExecutiveContext {
336
389
 
337
390
  // ── Pending message ──────────────────────────────────────────
338
391
 
392
+ /**
393
+ * A compound action the mind names as one thing it does — "when I do A then B,
394
+ * that is <name>". Registered into the SchemaRepertoire as a composite, after
395
+ * which it competes as a single affordance and can proceduralize into a habit.
396
+ *
397
+ * This is the creation seam for the instrumental→habitual gradient. Before it,
398
+ * `agency.composite.proposed` was subscribed by ReafferenceEngine — whose handler
399
+ * is the only caller of `registerComposite()` anywhere — and published by nothing,
400
+ * so no Will could ever hold a skill beyond the innate floor (#114).
401
+ */
402
+ export interface ProposedSkill {
403
+ /** What the mind calls it. Becomes the schema id. */
404
+ id: string
405
+ /** The sub-schemas it is made of, in order. Two or more, or it is not compound. */
406
+ composedOf: string[]
407
+ tags?: string[]
408
+ cost?: number
409
+ }
410
+
339
411
  export interface PendingMessage {
340
412
  id: string
341
413
  content: string
@@ -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)}`