@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
@@ -20,7 +20,7 @@
20
20
  * domains (see §6 cross-modal note).
21
21
  *
22
22
  * - `AuditionEngine` extends this and overrides `publishes()`/`snapshot()` for its
23
- * extra `audition.task.signal` schema and session snapshot.
23
+ * extra `executive.facet.handoff` schema and session snapshot.
24
24
  * - The four shell engines extend `ShellSenseEngine` (below) and are ~6 lines each.
25
25
  */
26
26
 
@@ -29,7 +29,10 @@ import type { CognitiveEvent, CognitiveBus } from '#cognition/bus'
29
29
  import type { CognitiveEventSchema } from '#cognition/schema.registry'
30
30
  import type { StateCommands } from '#core/types'
31
31
  import type { AccessGrants } from '#agency/access.grants'
32
- import type { SenseEngine, SensoryInput, SenseDomain, Percept } from '#senses/index'
32
+ import type { SensorySignal } from '#senses/provenance'
33
+ import { perceptEntity, type PerceptEntity } from '#cognition/percept.entity'
34
+ import { fnv1a } from '#agency/consequence'
35
+ import type { SenseEngine, SensoryInput, SenseDomain, Percept, Transduced } from '#senses/index'
33
36
 
34
37
  export abstract class BaseSenseEngine implements SenseEngine {
35
38
  abstract readonly name: string
@@ -38,7 +41,7 @@ export abstract class BaseSenseEngine implements SenseEngine {
38
41
  /**
39
42
  * The `SensoryInput.kind` values this engine consumes. Inputs of any other
40
43
  * kind are ignored silently by `ingest()` (no warning, no work) — this is how
41
- * a single `ingestSensory(domain, input)` call can be routed leniently.
44
+ * a single `senseSignal(domain, input)` call can be routed leniently.
42
45
  */
43
46
  protected abstract readonly acceptedKinds: ReadonlySet<SensoryInput['kind']>
44
47
 
@@ -51,12 +54,45 @@ export abstract class BaseSenseEngine implements SenseEngine {
51
54
 
52
55
  protected _bus: CognitiveBus | null = null
53
56
  protected _grants: AccessGrants | null = null
57
+ protected _trace: (( e: PerceptEntity ) => void) | null = null
58
+ protected _now: (() => number) | null = null
59
+
60
+ /**
61
+ * Whether this sense lays down a `percept` trace in state (SIGNAL_BOUNDARY P0).
62
+ *
63
+ * ON by default, because that is the contract a host is owed: implement a
64
+ * sense, and what it senses reaches the five things that read percepts — the
65
+ * rupture gate, reafference credit, working memory, the executive prompt, and
66
+ * novelty. Before this, `publishPercept()` emitted a bus event three
67
+ * subscribers glanced at for one tick and nothing else, so a robot host
68
+ * ingesting frames could never remember having SEEN anything.
69
+ *
70
+ * `AuditionEngine` overrides it to `false` — see the comment there. It is the
71
+ * documented exception, not the template.
72
+ */
73
+ protected readonly tracesPercepts: boolean = true
54
74
 
55
75
  // ── Wiring ───────────────────────────────────────────────
56
76
  attachBus( bus: CognitiveBus ): void { this._bus = bus }
57
77
  /** Inject the AccessGrants so `ingest()` honours `gateEffector` (the permission gate). */
58
78
  attachGrants( g: AccessGrants ): void { this._grants = g }
59
79
 
80
+ /**
81
+ * Wire the sense to state: where a percept goes, and what tick it is now.
82
+ *
83
+ * Both together, never one: a percept without a tick is uncollectable — the
84
+ * sweeper reads `metadata.tick` and nothing else — which is the leak P0 step 2
85
+ * closed in two other writers. And the tick has to be injected rather than
86
+ * remembered, because a sense is INGEST-DRIVEN and off-tick: it has no `react()`
87
+ * to be handed one in. Audition's own `_lastDecisionTick` is the cautionary
88
+ * case — it lags to whenever the executive last decided, so a message arriving
89
+ * forty ticks later would be stamped forty ticks stale and swept on arrival.
90
+ */
91
+ attachPerceptTrace( write: ( e: PerceptEntity ) => void, currentTick: () => number ): void {
92
+ this._trace = write
93
+ this._now = currentTick
94
+ }
95
+
60
96
  // ── CognitiveEngine defaults ─────────────────────────────
61
97
  publishes(): CognitiveEventSchema[] {
62
98
  return [ { type: `senses.${this.domain}.percept`, version: 1, validate: () => null } ]
@@ -71,7 +107,7 @@ export abstract class BaseSenseEngine implements SenseEngine {
71
107
  * the domain-specific work to `_perceive()`. Subclasses never re-handle gating
72
108
  * or filtering — they only implement `_perceive()`.
73
109
  */
74
- async ingest( input: SensoryInput ): Promise<void> {
110
+ async sense( input: SensoryInput ): Promise<void> {
75
111
  // Permission gate — AccessGrants decides whether this sense may ingest.
76
112
  if( this.gateEffector && this._grants && !this._grants.isAllowed( this.gateEffector ) ) return
77
113
  if( !this.acceptedKinds.has( input.kind ) ) return
@@ -86,15 +122,77 @@ export abstract class BaseSenseEngine implements SenseEngine {
86
122
  * Publish a percept on this engine's `senses.<domain>.percept` topic. The
87
123
  * single emit chokepoint — the AttentionAllocator (and, in future, a
88
124
  * cross-modal binder) observes percepts here.
125
+ *
126
+ * `from` is the signal this percept was transduced FROM, and it is required
127
+ * rather than optional on purpose: every percept has a cause, and the one
128
+ * fact transduction must not lose is whose doing that cause was. Taking it
129
+ * here — instead of stashing the in-flight input on a field — is what keeps
130
+ * the stamp correct while `_perceive()` is async and two ingests overlap.
131
+ *
132
+ * The percept arrives as `Transduced`, i.e. WITHOUT the two fields, so a
133
+ * sense engine cannot supply them at all. An earlier cut let one through: the
134
+ * stamp overwrote `provenance` unconditionally but `sourceIntentId` only when
135
+ * the host supplied one, so an engine could fabricate an intent id and it
136
+ * survived — provenance the mind would later trust, laundered by the very
137
+ * step that exists to establish it. The type now refuses it outright; the
138
+ * host's assertion is the only authority.
139
+ *
140
+ * Returns the stamped percept so a sense engine that needs it downstream (as
141
+ * audition does, to route the turn) uses the SAME object the bus saw, rather
142
+ * than a second one that could drift from it.
89
143
  */
90
- protected publishPercept( percept: Percept ): void {
144
+ protected publishPercept<P extends Percept>( percept: Transduced<P>, from: SensorySignal ): P {
145
+ // Stripped, then re-applied — belt AND braces. `Transduced` makes forging a
146
+ // compile error, but a type is only as strong as the compiler that saw it:
147
+ // a JS host, or a consumer built against an older .d.ts, hands over whatever
148
+ // it likes. The strip is what holds at runtime.
149
+ const { provenance: _stale, sourceIntentId: _staleIntent, ...rest } =
150
+ percept as Transduced<P> & Partial<SensorySignal>
151
+
152
+ const stamped = {
153
+ ...rest,
154
+ provenance: from.provenance,
155
+ ...( from.sourceIntentId ? { sourceIntentId: from.sourceIntentId } : {} ),
156
+ } as P
157
+
91
158
  this._bus?.publish({
92
159
  type: `senses.${this.domain}.percept`,
93
160
  version: 1,
94
161
  sourceEngine: this.name,
95
- salience: percept.salience,
96
- payload: percept,
162
+ salience: stamped.salience,
163
+ payload: stamped,
97
164
  })
165
+
166
+ this._writeTrace( stamped )
167
+ return stamped
168
+ }
169
+
170
+ /**
171
+ * Lay the percept down in state, so it reaches the faculties that read
172
+ * percepts rather than only the three that were listening on the bus this
173
+ * tick. Silent when the sense opts out or the host wired no sink.
174
+ *
175
+ * The id is content-derived and tick-stamped — never `wallClock()` — because
176
+ * this entity lives in state, and a wall-clock id makes a recorded and a
177
+ * replayed run diverge (R2). Two identical signals from one entity on one
178
+ * tick collapse to one percept, which is the same coalescing audition already
179
+ * applies to a burst of identical messages.
180
+ */
181
+ private _writeTrace( p: Percept ): void {
182
+ if( !this.tracesPercepts || !this._trace || !this._now ) return
183
+
184
+ const tick = this._now()
185
+ this._trace( perceptEntity( {
186
+ id: `sense-${ this.domain }-${ tick }-${ fnv1a( `${ p.sourceEntityId }\u0000${ p.summary }` ) }`,
187
+ tick,
188
+ salience: p.salience,
189
+ category: this.domain,
190
+ summary: p.summary,
191
+ provenance: p.provenance,
192
+ entityId: p.sourceEntityId,
193
+ ...( p.sourceIntentId !== undefined ? { sourceIntentId: p.sourceIntentId } : {} ),
194
+ ...( p.data !== undefined ? { data: p.data } : {} ),
195
+ } ) )
98
196
  }
99
197
  }
100
198
 
@@ -21,6 +21,7 @@
21
21
 
22
22
  import type { CognitiveEngine } from '#cognition/types'
23
23
  import type { CognitiveBus } from '#cognition/bus'
24
+ import type { SensorySignal } from './provenance'
24
25
 
25
26
  // ── Domain ────────────────────────────────────────────────────
26
27
 
@@ -31,15 +32,57 @@ export type SenseDomain =
31
32
  | 'olfaction' // ambient signals — OlfactionEngine (shell)
32
33
  | 'gustation' // self-evaluation — GustationEngine (shell)
33
34
 
35
+ // ── Provenance (re-exported from ./provenance) ────────────────
36
+
37
+ export type { SignalProvenance, SensorySignal, Transduced } from './provenance'
38
+ export { asProvenance } from './provenance'
39
+
34
40
  // ── Percept types ─────────────────────────────────────────────
35
41
 
36
- /** Base percept published on the CognitiveBus. */
37
- export interface Percept {
42
+ /**
43
+ * Base percept published on the CognitiveBus.
44
+ *
45
+ * It extends `SensorySignal` because transduction does not change whose doing a
46
+ * signal was: what the host stamped on the input is what the percept carries.
47
+ * `BaseSenseEngine.publishPercept()` applies both from the input, so a percept
48
+ * ALWAYS carries provenance — no consumer downstream ever writes a fallback.
49
+ */
50
+ export interface Percept extends SensorySignal {
38
51
  domain: SenseDomain
39
52
  sourceEntityId: string
40
53
  timestamp: number
41
54
  salience: number // 0–1, computed by the sense engine
42
55
  raw: unknown // original input object
56
+ /**
57
+ * A LABEL for what was sensed. Required, so a percept is never invisible —
58
+ * `extractPercepts` skips one without it.
59
+ *
60
+ * A label, not the payload, and not a meaning. The engine composes it from
61
+ * what it has (the signal's name, a compact rendering) — A HOST IS NEVER
62
+ * ASKED TO WRITE ONE. Demanding prose from an integration puts the mind's own
63
+ * work on the wrong side of the boundary: a robot's vision system reports
64
+ * `object_detected, confidence 0.9, bbox […]`, and making it also write
65
+ * "I see a red ball on the table" is asking the arm to do the thinking.
66
+ *
67
+ * Because the ENGINE composes this, `PERCEPT_SUMMARY_CAP` may bound it —
68
+ * bounding its own words destroys nobody's only copy. `data` beside it is the
69
+ * host's, and is never bounded.
70
+ */
71
+ summary: string
72
+ /**
73
+ * What the host actually sent, in the shape it had it — whole, unreshaped,
74
+ * uncapped.
75
+ *
76
+ * The mind's job is to make meaning by connecting pieces of information, and
77
+ * it cannot do that from a sentence somebody else wrote about the pieces. A
78
+ * host reporting `memberCount: 47` under a summary saying "a few people"
79
+ * leaves a mind that can never recover 47.
80
+ *
81
+ * Distinct from `raw`, which is the whole `SensoryInput` envelope and is
82
+ * engine-internal — never persisted. This is, and it reaches state, working
83
+ * memory and the prompt.
84
+ */
85
+ data?: unknown
43
86
  }
44
87
 
45
88
  /** Audition-specific percept — carries language content and thread context. */
@@ -56,16 +99,43 @@ export interface LanguagePercept extends Percept {
56
99
  // ── Sensory input types (discriminated union) ─────────────────
57
100
 
58
101
  // Audition
59
- export interface TextMessage {
102
+ export interface TextMessage extends SensorySignal {
60
103
  kind: 'text'
61
104
  entityId: string
62
105
  threadId: string
63
106
  content: string
64
107
  /** Display name — used in the facet focus content. */
65
108
  speakerName?: string
109
+ /**
110
+ * True when `threadId` is a PRIVATE thread — this someone and the mind, nobody
111
+ * else listening. The single fact that decides whether a room is the right
112
+ * place for a given utterance, and the Discord edge has always computed it
113
+ * (`isDM`) and discarded it before the mind could see it: a follow-up promised
114
+ * in a DM went out to a public channel, because the roster's "where did I last
115
+ * see them" is a different question from "where did I promise this".
116
+ *
117
+ * Undefined means the channel did not say, which is honestly different from
118
+ * false — an unknown room is not known to be public.
119
+ */
120
+ direct?: boolean
121
+ /**
122
+ * What this room is CALLED, where the channel knows.
123
+ *
124
+ * A room has had a dossier of its own since 0.9.0 and no way to be named, so
125
+ * every place the mind knew was the id it was reached at —
126
+ * `discord:1531261362838441996`. It rendered to the mind as "something", which
127
+ * is what the prompt says for a `thing` with no name, and left a mind deciding
128
+ * *where* to say something choosing between two opaque numbers.
129
+ *
130
+ * A display label, not an address: `#general` is what a person calls the room,
131
+ * `discord:1531…` is how a message gets there, and 0.9.0 established that those
132
+ * are different facts. The bridge composes it, because what a room is called is
133
+ * a platform's business and the mind should not learn Discord's spelling.
134
+ */
135
+ threadName?: string
66
136
  }
67
137
 
68
- export interface VoiceChunk {
138
+ export interface VoiceChunk extends SensorySignal {
69
139
  kind: 'voice'
70
140
  entityId: string
71
141
  threadId: string
@@ -74,14 +144,14 @@ export interface VoiceChunk {
74
144
  }
75
145
 
76
146
  // Vision (stub)
77
- export interface ImageFrame {
147
+ export interface ImageFrame extends SensorySignal {
78
148
  kind: 'image'
79
149
  entityId: string
80
150
  data: Buffer
81
151
  mimeType: string
82
152
  }
83
153
 
84
- export interface VideoSegment {
154
+ export interface VideoSegment extends SensorySignal {
85
155
  kind: 'video'
86
156
  entityId: string
87
157
  frames: ImageFrame[]
@@ -89,41 +159,41 @@ export interface VideoSegment {
89
159
  }
90
160
 
91
161
  // Somatosensation (stub)
92
- export interface WebhookEvent {
162
+ export interface WebhookEvent extends SensorySignal {
93
163
  kind: 'webhook'
94
164
  source: string
95
165
  payload: unknown
96
166
  headers: Record<string, string>
97
167
  }
98
168
 
99
- export interface SystemSignal {
169
+ export interface SystemSignal extends SensorySignal {
100
170
  kind: 'system'
101
171
  signal: string
102
172
  data: unknown
103
173
  }
104
174
 
105
175
  // Olfaction (stub)
106
- export interface AmbientMetric {
176
+ export interface AmbientMetric extends SensorySignal {
107
177
  kind: 'ambient'
108
178
  metricKey: string
109
179
  value: number
110
180
  trend: 'rising' | 'falling' | 'stable'
111
181
  }
112
182
 
113
- export interface BackgroundSignal {
183
+ export interface BackgroundSignal extends SensorySignal {
114
184
  kind: 'background'
115
185
  category: string
116
186
  data: unknown
117
187
  }
118
188
 
119
189
  // Gustation (stub)
120
- export interface InternalEvaluation {
190
+ export interface InternalEvaluation extends SensorySignal {
121
191
  kind: 'self-eval'
122
192
  context: string
123
193
  trigger: string
124
194
  }
125
195
 
126
- export interface SelfAssessmentTrigger {
196
+ export interface SelfAssessmentTrigger extends SensorySignal {
127
197
  kind: 'assessment'
128
198
  goalId?: string
129
199
  checkType: string
@@ -159,7 +229,7 @@ export type SensoryInput =
159
229
  export interface SenseEngine extends CognitiveEngine {
160
230
  readonly domain: SenseDomain
161
231
  attachBus( bus: CognitiveBus ): void
162
- ingest( input: SensoryInput ): Promise<void>
232
+ sense( input: SensoryInput ): Promise<void>
163
233
  }
164
234
 
165
235
  // ── Re-exports ────────────────────────────────────────────────
@@ -0,0 +1,128 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/senses/provenance.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * Signal provenance — the boundary vocabulary for "whose doing was this?"
7
+ * (SIGNAL_BOUNDARY P0a).
8
+ *
9
+ * Its own module, not part of `senses/index`, for one concrete reason:
10
+ * `BaseSenseEngine` needs the VALUE `provenanceOf` at the emit chokepoint, and
11
+ * `senses/index` re-exports `BaseSenseEngine` — importing it from there would
12
+ * be a runtime import cycle rather than the erased type-only one that was there
13
+ * before. `senses/index` re-exports everything here, so the public path is
14
+ * unchanged.
15
+ */
16
+
17
+ /**
18
+ * Whether a signal arriving at the senses was caused by this mind's own
19
+ * efference (SIGNAL_BOUNDARY P0a).
20
+ *
21
+ * • 'exafferent' — the world did this. Nothing of mine preceded it.
22
+ * • 'reafferent' — I did this, and I am now sensing the consequence.
23
+ * • 'unknown' — the source cannot say.
24
+ *
25
+ * There is no fourth, silent state — see `SensorySignal`.
26
+ *
27
+ * ONE CONCEPT, TWO WAYS OF ARRIVING AT IT. This type is shared with the percept
28
+ * entities `exteroception` and `outbox.controller` write, which used the same
29
+ * two spellings before it existed (EXAFFERENCE P2/P3), and it is deliberately
30
+ * not "asserted only":
31
+ *
32
+ * • At the WORLD door, provenance is INFERRED and that is correct — the mind
33
+ * holds both sides. `Exteroception` matches an observed change against its
34
+ * own live consequence descriptors (`matchConsequenceText` /
35
+ * `matchConsequenceEntity`) and tags the hit `'reafferent'`.
36
+ * • At the SENSE door, provenance must be ASSERTED — there is nothing to
37
+ * match against. A host's Discord message and the mind's own echo of it are
38
+ * identical from the inside; only the host can tell them apart.
39
+ *
40
+ * What the two share is the consequence, and it is load-bearing:
41
+ * `action.selector`'s rupture gate counts only `'exafferent'` percepts, so
42
+ * anything tagged (or defaulted) as mine can never rupture a commitment.
43
+ */
44
+ export type SignalProvenance = 'exafferent' | 'reafferent' | 'unknown'
45
+
46
+ /**
47
+ * What EVERY sensory signal carries, whatever kind it is — the provenance stamp.
48
+ *
49
+ * It lives on the input rather than being derived downstream because only the
50
+ * host knows. A Discord bridge knows that the message it just received is the
51
+ * webhook echo of the message the mind sent; nothing inside the mind can tell
52
+ * that from a stranger saying the same words. So the boundary asserts it, and
53
+ * the mind trusts the assertion.
54
+ *
55
+ * WHY `provenance` IS REQUIRED AND HAS NO DEFAULT. It shipped optional first,
56
+ * defaulting to `'exafferent'`, and that was wrong for a reason worth keeping
57
+ * written down. This codebase makes a field optional when absence is the ONLY
58
+ * way to say a third thing: `speakerName?` (no string means "no name learned"),
59
+ * `direct?` (no boolean means "the channel did not say"). Provenance already
60
+ * HAS its third thing — `'unknown'`. So optionality bought a fourth state that
61
+ * was behaviourally identical to `'exafferent'` and epistemically its opposite:
62
+ * a claim that the world did this, made by nobody. Worse, it was lossy in the
63
+ * one direction that costs — a reader could never separate "the host asserts
64
+ * exafference" from "the host said nothing", which want different treatment
65
+ * downstream, and `'unknown'` exists precisely to keep them apart.
66
+ *
67
+ * The related-but-different precedent is `asFinality()`. That one DOES default,
68
+ * because it normalizes an `unknown` read back off entity metadata or a host
69
+ * ack, where the type system cannot help and something must be chosen. Here the
70
+ * type system can help, so it does. Should provenance ever need reading off a
71
+ * tape, that reader gets its own `asFinality`-shaped normalizer, and the
72
+ * direction of ITS fallback is `'exafferent'`: a percept wrongly marked as mine
73
+ * is attenuated and can never rupture a commitment, so a mind that mislabels
74
+ * the world as its own doing goes quiet about real events. Erring the other way
75
+ * costs only a little redundant salience.
76
+ *
77
+ * `sourceIntentId` stays optional by the same rule that made `provenance`
78
+ * required: there is no id meaning "no intent", so absence is the only way to
79
+ * say it, and it is meaningless unless `provenance` is `'reafferent'`.
80
+ */
81
+ export interface SensorySignal {
82
+ /** Mine, the world's, or honestly unknown. Asserted by the host — never inferred. */
83
+ provenance: SignalProvenance
84
+ /**
85
+ * The `agency.intent` id this is the consequence of. Only meaningful when
86
+ * `provenance` is `'reafferent'`; the correlation handle a later mechanism
87
+ * needs to ask *"is this the echo of that?"* (`ACT_EXPECTATIONS`).
88
+ */
89
+ sourceIntentId?: string
90
+ }
91
+
92
+ /**
93
+ * A percept as a sense engine BUILDS it — provenance omitted, because it is not
94
+ * the engine's to write. `publishPercept()` applies the host's assertion, so
95
+ * this type is what makes laundering a compile error rather than a runtime one.
96
+ */
97
+ export type Transduced<P extends { provenance: SignalProvenance }> =
98
+ Omit<P, 'provenance' | 'sourceIntentId'>
99
+
100
+ /**
101
+ * Normalize an UNTYPED provenance — one arriving over a protocol the type
102
+ * system cannot police: an MCP tool call, an HTTP body, a wire envelope, a
103
+ * value read back off a snapshot.
104
+ *
105
+ * This is the `asFinality()` situation, and the ONLY situation in which
106
+ * provenance has a default. Inside the package the field is required, because
107
+ * there the compiler can make a producer answer. At an untyped boundary it
108
+ * cannot, something must be chosen, and every such read goes through here
109
+ * rather than comparing string literals at the call site.
110
+ *
111
+ * THE DEFAULT IS 'exafferent', AND NEVER 'reafferent'. A percept wrongly marked
112
+ * as mine is attenuated on the exafference path and can never rupture a
113
+ * commitment — so a mind that mislabels the world as its own doing goes quiet
114
+ * about real events, which is the failure that matters. Erring the other way
115
+ * costs only a little redundant salience. 'reafferent' is a claim only a caller
116
+ * that actually knows can make.
117
+ *
118
+ * Note it does NOT fall back to 'unknown', which would be the tidy-looking
119
+ * choice. 'unknown' is an assertion too — "I looked and I cannot tell" — and a
120
+ * caller that simply did not send the field has not looked. Where the ABSENCE
121
+ * is structural rather than a caller's omission (a wire envelope with no such
122
+ * field), say 'unknown' at that site explicitly instead of routing through here.
123
+ */
124
+ export function asProvenance( raw: unknown ): SignalProvenance {
125
+ return raw === 'reafferent' ? 'reafferent'
126
+ : raw === 'unknown' ? 'unknown'
127
+ : 'exafferent'
128
+ }
@@ -3,21 +3,129 @@
3
3
  // ─────────────────────────────────────────────────────────────
4
4
 
5
5
  /**
6
- * SomatosensationEngine [SHELL] touch / physical-world interaction.
6
+ * SomatosensationEngine — the sense that feels things happen TO the mind.
7
7
  *
8
- * Handles webhook events, system signals, and external API callbacks Will's
9
- * "touch", its awareness of interaction with external systems. Future: route
10
- * incoming webhooks as percepts into ThreatEvaluator / RewardEvaluator.
8
+ * Webhooks, system signals, external callbacks: not something said, not
9
+ * something seen, but the world touching the mind directly. It was the first
10
+ * shell to be implemented because the wake event needed a door — SIGNAL_BOUNDARY
11
+ * P1 — and a hand-written percept in `stem/index.ts` was the bypass that proved
12
+ * the door was missing.
11
13
  *
12
- * Currently a structural stub `_perceive()` (inherited from ShellSenseEngine)
13
- * logs a warning and returns. The base handles gate / filter / publish / boilerplate.
14
+ * WHAT MAKES THIS A SENSE RATHER THAN A HELPER. It does nothing the other senses
15
+ * do not: transduce an input into a `Percept` with a salience and a summary, and
16
+ * hand it to `publishPercept`, which stamps provenance from the host's assertion
17
+ * and lays down the trace. Everything downstream — the rupture gate, working
18
+ * memory, the executive prompt, novelty — receives it because it is a percept,
19
+ * not because anyone wired those five places to a wake event.
20
+ *
21
+ * That is the whole point of a door: the wake event stops being special.
14
22
  */
15
23
 
16
- import type { SensoryInput } from '#senses/index'
17
- import { ShellSenseEngine } from '#senses/base.sense.engine'
24
+ import type { SensoryInput, SystemSignal, WebhookEvent, Percept, Transduced } from '#senses/index'
25
+ import { BaseSenseEngine } from '#senses/base.sense.engine'
26
+ import { PERCEPT_SUMMARY_CAP } from '#cognition/percept.entity'
27
+
28
+ /**
29
+ * How loud a system signal is by default.
30
+ *
31
+ * A signal arrives because something HAPPENED to the mind — a host woke it, a
32
+ * webhook fired — so it outranks the ambient world-change percepts
33
+ * `exteroception` produces at 0.3, and clears `action.selector`'s rupture gate
34
+ * (0.4). A host that knows better says so with `salience` on the input.
35
+ */
36
+ export const SYSTEM_SIGNAL_SALIENCE = 0.75
18
37
 
19
- export class SomatosensationEngine extends ShellSenseEngine {
38
+ export class SomatosensationEngine extends BaseSenseEngine {
20
39
  readonly name = 'somatosensation-engine'
21
40
  readonly domain = 'somatosensation' as const
22
41
  protected readonly acceptedKinds = new Set<SensoryInput['kind']>( [ 'webhook', 'system' ] )
42
+
43
+ protected async _perceive( input: SensoryInput ): Promise<void> {
44
+ const percept: Transduced<Percept> = input.kind === 'system'
45
+ ? this._fromSignal( input as SystemSignal )
46
+ : this._fromWebhook( input as WebhookEvent )
47
+
48
+ this.publishPercept( percept, input )
49
+ }
50
+
51
+ private _fromSignal( s: SystemSignal ): Transduced<Percept> {
52
+ return {
53
+ domain: this.domain,
54
+ // The signal IS the source: nothing in the world sent it, the substrate did.
55
+ sourceEntityId: `system:${ s.signal }`,
56
+ timestamp: 0,
57
+ salience: salienceOf( s.data, SYSTEM_SIGNAL_SALIENCE ),
58
+ summary: labelFor( s.signal, s.data ),
59
+ ...( s.data !== undefined && s.data !== null ? { data: s.data } : {} ),
60
+ raw: s,
61
+ }
62
+ }
63
+
64
+ private _fromWebhook( w: WebhookEvent ): Transduced<Percept> {
65
+ return {
66
+ domain: this.domain,
67
+ sourceEntityId: `webhook:${ w.source }`,
68
+ timestamp: 0,
69
+ salience: salienceOf( w.payload, SYSTEM_SIGNAL_SALIENCE ),
70
+ summary: labelFor( w.source, w.payload ),
71
+ ...( w.payload !== undefined && w.payload !== null ? { data: w.payload } : {} ),
72
+ raw: w,
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * A LABEL for a signal — what arrived, not what it means.
79
+ *
80
+ * The mind makes the meaning; this only has to make the percept legible enough
81
+ * to be noticed and connected to its data. A HOST IS NEVER ASKED FOR PROSE:
82
+ * demanding a sentence from a robot's control layer puts the mind's own work on
83
+ * the wrong side of the integration boundary, and a lidar driver has no business
84
+ * describing what a scan implies.
85
+ *
86
+ * The signal's own NAME is the hint, and it is free — `discord_server_snapshot`,
87
+ * `WAKE`, `lidar.scan` already say what kind of thing this is. A host that
88
+ * happens to have words may put a `summary` on its data and they are used
89
+ * instead; it is an option, never an obligation.
90
+ *
91
+ * Bounded by `PERCEPT_SUMMARY_CAP` because the ENGINE writes it. The data it
92
+ * labels is beside it, whole and uncapped.
93
+ */
94
+ function labelFor( signal: string, data: unknown ): string {
95
+ const words = hostWords( data )
96
+ if( words ) return words.length > PERCEPT_SUMMARY_CAP
97
+ ? `${ words.slice( 0, PERCEPT_SUMMARY_CAP - 1 ) }\u2026` : words
98
+
99
+ const rendered = compact( data )
100
+ const label = rendered ? `${ signal }: ${ rendered }` : `Something happened: ${ signal }.`
101
+ return label.length > PERCEPT_SUMMARY_CAP
102
+ ? `${ label.slice( 0, PERCEPT_SUMMARY_CAP - 1 ) }\u2026` : label
103
+ }
104
+
105
+ /** A host's own words, if it chose to offer any. Optional, never required. */
106
+ function hostWords( data: unknown ): string | undefined {
107
+ if( typeof data === 'string') return data.length > 0 ? data : undefined
108
+ if( typeof data === 'object' && data !== null && !Array.isArray( data ) ){
109
+ const s = ( data as Record<string, unknown> )['summary']
110
+ if( typeof s === 'string' && s.length > 0 ) return s
111
+ }
112
+ return undefined
113
+ }
114
+
115
+ /** A glance at the shape, for the label only. The whole thing rides in `data`. */
116
+ function compact( data: unknown ): string | undefined {
117
+ if( data === undefined || data === null ) return undefined
118
+ if( typeof data !== 'object') return String( data )
119
+ try {
120
+ const json = JSON.stringify( data )
121
+ return json === '{}' || json === '[]' ? undefined : json
122
+ }
123
+ catch { return undefined } // circular — the label falls back to the name
124
+ }
125
+
126
+ /** Same door for salience: the host may say, otherwise the default stands. */
127
+ function salienceOf( data: unknown, fallback: number ): number {
128
+ if( typeof data !== 'object' || data === null || Array.isArray( data ) ) return fallback
129
+ const s = ( data as Record<string, unknown> )['salience']
130
+ return typeof s === 'number' && Number.isFinite( s ) ? Math.max( 0, Math.min( 1, s ) ) : fallback
23
131
  }