@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
@@ -11,7 +11,7 @@
11
11
  * ExecutiveFacet — one facet per entityId, kept alive for the session duration.
12
12
  *
13
13
  * Architecture:
14
- * - External call: WillManager.ingestText() → audition.ingest(TextMessage)
14
+ * - External call: WillManager.senseText() → audition.sense(TextMessage)
15
15
  * - Percept published on bus: senses.audition.percept
16
16
  * - Facet handoffs to the seat: executive.facet.handoff (escalation | undertaking)
17
17
  * - GoalManager integration: automatic via executive.facet.progress (bus)
@@ -73,10 +73,13 @@ import type {
73
73
  SensoryInput,
74
74
  LanguagePercept,
75
75
  TextMessage,
76
+ Transduced,
76
77
  VoiceChunk
77
78
  } from '#senses/index'
79
+ import { PERCEPT_SUMMARY_CAP } from '#cognition/percept.entity'
78
80
  import { validateFacetHandoff, type HandoffBody } from '#faculties/executive.engine/escalation.buffer'
79
81
  import { fnv1a } from '#agency/consequence'
82
+ import type { OutreachResult } from '#agency/engines/motor.schema.executor'
80
83
 
81
84
  // ── Internal types ─────────────────────────────────────────────
82
85
 
@@ -85,6 +88,15 @@ interface ConversationDecision {
85
88
  reply: string
86
89
  /** Individual reply bubbles for display (separate SSE chunks). */
87
90
  replyBubbles: string[]
91
+ /**
92
+ * The mind DECLARED silence — it considered speaking and chose not to.
93
+ *
94
+ * Distinct from empty bubbles, which also happens when authoring timed out, the
95
+ * facet budget was full, or a second pass deferred to one already in flight.
96
+ * Only this one is an answer; the others are the absence of one, and the
97
+ * executor must keep holding those.
98
+ */
99
+ withheld?: boolean
88
100
  targetEntityId: string
89
101
  /**
90
102
  * Actions this facet aimed at someone OTHER than the person it is talking to.
@@ -178,7 +190,7 @@ export function partitionOutwardIntents(
178
190
  // [REPLY_TEXT] is plain prose — no JSON wrapper, no targetEntityId needed.
179
191
  // The facet is entity-scoped so the recipient is always the speakerEntityId.
180
192
 
181
- const CONVERSATION_OUTPUT_FORMAT = `\
193
+ export const CONVERSATION_OUTPUT_FORMAT = `\
182
194
  ## Response Format (REQUIRED)
183
195
 
184
196
  Step 1 — JSON object (my private reasoning, optionally in a \`\`\`json code block):
@@ -217,6 +229,24 @@ That is recorded and NEVER sent. Anything between the [REPLY_TEXT] markers IS SE
217
229
  like "[no message this cycle — waiting for their reply]" does not describe my silence to
218
230
  myself, it delivers that sentence to them. If I write both blocks, the silence wins.
219
231
 
232
+ ## Reaching someone who is not in this conversation
233
+ [REPLY_TEXT] is delivered TO THE PERSON I AM TALKING TO, and to nobody else. It has an
234
+ audience, not just a reader. So words meant for a third party do not become a message to
235
+ that third party by being about them — they are handed to the speaker, who reads something
236
+ addressed to someone else, while the person it was actually for never hears it.
237
+
238
+ To reach someone else I name them in an action:
239
+
240
+ {"type": "reach-out", "target": "<their name or id as it appears under '## People I Know'>",
241
+ "args": {"content": "what I want to say to them"}}
242
+
243
+ I am one conversation of a mind that is having several. Opening a channel is not mine to
244
+ do — that action hands the intention to the part of me that owns whom I contact. It is a
245
+ decision, not a delivery: at the moment I write it nothing has reached anyone, and it may
246
+ yet come to nothing. So I never tell the person in front of me that I have contacted
247
+ someone, or that a message is on its way. The most I can truthfully say is that I mean to.
248
+ I keep [REPLY_TEXT] for the person in front of me.
249
+
220
250
  ## When to use GOALS_NEW (almost always)
221
251
  If the speaker requests, mentions, or implies something I should follow through on — embed [GOALS_NEW] in my reasoning.
222
252
  This tracks intent across future cycles on its own.
@@ -347,6 +377,27 @@ export class AuditionEngine extends BaseSenseEngine {
347
377
  * off-tick engine has. Stamped from `FacetDecision.tick`, and used to key the
348
378
  * conversation records it writes into state.
349
379
  */
380
+ /**
381
+ * Audition does NOT lay down a percept trace (SIGNAL_BOUNDARY P0, step 3).
382
+ *
383
+ * Every other sense does, and a host implementing a new one gets it by
384
+ * default. This is the grandfathered exception, and the reason is measurement
385
+ * rather than principle: audition is the only live sense, and switching it on
386
+ * routes every inbound message to five consumers it has never reached —
387
+ * `action.selector`'s rupture gate (where a fresh high-salience exafferent
388
+ * percept can preempt an awaiting intent), working memory, the executive
389
+ * prompt, novelty, and reafference credit. On a deployed Will that is not a
390
+ * tweak; it is a different mind, and it deserves a measured rollout rather
391
+ * than a line in a refactor.
392
+ *
393
+ * Audition is not trace-less meanwhile: it writes `conversation.received`
394
+ * through its own sink, which is what SocialPerception reads. That record is
395
+ * social — sourceKeid, directedAtSelf, action:'communication' — and is NOT
396
+ * the generic percept the other senses now write. Two different traces for
397
+ * two different readers; this flag turns off only the second.
398
+ */
399
+ protected readonly tracesPercepts = false
400
+
350
401
  private _lastDecisionTick = 0
351
402
  /** Speaker attachment strength accessor (0–1) — weights salience by relationship. */
352
403
  private _getAttachmentScore: (( entityId: string ) => number) | null = null
@@ -557,7 +608,7 @@ export class AuditionEngine extends BaseSenseEngine {
557
608
  const salience = this._model.observe(`audition.${entityId}`, langEnergy ).salience
558
609
 
559
610
  // ── Percept ────────────────────────────────────────────────
560
- const percept: LanguagePercept = {
611
+ const transduced: Transduced<LanguagePercept> = {
561
612
  domain: 'audition',
562
613
  channel: msg.kind,
563
614
  content,
@@ -565,6 +616,10 @@ export class AuditionEngine extends BaseSenseEngine {
565
616
  speakerEntityId: entityId,
566
617
  threadId,
567
618
  digest: this._digests.getDigest( threadId ),
619
+ // What a heard turn amounts to, for readers that do not know this is
620
+ // audition. Bounded, because `summary` renders into the executive prompt
621
+ // and a pasted essay would take the whole percept budget.
622
+ summary: `${ speakerName } said: ${ content }`.slice( 0, PERCEPT_SUMMARY_CAP ),
568
623
  salience,
569
624
  // Arrival metadata for an EXTERNAL inbound message (network/RPC boundary):
570
625
  // no sim clock in scope here and the value is not replayed — wallClock() is
@@ -575,7 +630,7 @@ export class AuditionEngine extends BaseSenseEngine {
575
630
 
576
631
  // Publish to CognitiveBus — AttentionAllocator et al. can react.
577
632
  // publishPercept() (base) is the single emit chokepoint on senses.<domain>.percept.
578
- this.publishPercept( percept )
633
+ const percept = this.publishPercept<LanguagePercept>( transduced, msg )
579
634
 
580
635
  // ── Update digest with inbound turn ───────────────────────
581
636
  this._digests.append( threadId, 'user', content )
@@ -640,6 +695,16 @@ export class AuditionEngine extends BaseSenseEngine {
640
695
  this._turnDone.get( entityId )?.()
641
696
  }
642
697
 
698
+ /**
699
+ * True while a turn with this person is still resolving — they spoke and the
700
+ * reply has not landed yet. The agency asks before delivering a self-initiated
701
+ * message (see OutreachAuthor.isSpeakingTo), so one mind does not reach one
702
+ * person down two paths in the same tick.
703
+ */
704
+ isSpeakingTo( entityId: string ): boolean {
705
+ return this._turnDone.has( entityId )
706
+ }
707
+
643
708
  // ── Facet lifecycle ─────────────────────────────────────────
644
709
 
645
710
  private async _routeToFacet( percept: LanguagePercept, speakerName: string ): Promise<boolean> {
@@ -845,8 +910,8 @@ export class AuditionEngine extends BaseSenseEngine {
845
910
  * (MotorSchemaExecutor) to deliver through the proactive communicate path; empty
846
911
  * when no executive is attached or the facet budget is full (caller then awaits).
847
912
  */
848
- async authorOutreach( entityId: string, entityName: string, gist?: string ): Promise<string[]> {
849
- if( !this._executiveEngine ) return []
913
+ async authorOutreach( entityId: string, entityName: string, gist?: string ): Promise<OutreachResult> {
914
+ if( !this._executiveEngine ) return { bubbles: [] }
850
915
 
851
916
  // One authoring pass per person at a time.
852
917
  //
@@ -862,7 +927,7 @@ export class AuditionEngine extends BaseSenseEngine {
862
927
  // had a chance to read it, which is the outcome we want anyway.
863
928
  if( this._outreachInFlight.has( entityId ) ){
864
929
  logger.info(`[audition-engine] already composing an outreach to ${ entityId } — not opening a second`)
865
- return []
930
+ return { bubbles: [] }
866
931
  }
867
932
 
868
933
  // Already talking to them? Then this is not a second thread — it is a thing to
@@ -889,7 +954,7 @@ export class AuditionEngine extends BaseSenseEngine {
889
954
  return spawned.handle
890
955
  } )()
891
956
 
892
- if( !handle ) return []
957
+ if( !handle ) return { bubbles: [] }
893
958
 
894
959
  if( openThread )
895
960
  logger.info(`[audition-engine] composing outreach to ${ entityId } inside the open conversation (${ openThread.facetId })`)
@@ -934,7 +999,7 @@ export class AuditionEngine extends BaseSenseEngine {
934
999
  // saying something — nobody is waiting on this.
935
1000
  if( output.noMessage !== undefined ){
936
1001
  logger.info(`[audition-engine] chose not to reach out to ${ entityName }: ${ output.noMessage.slice( 0, 120 ) }`)
937
- return { reply: '', replyBubbles: [], targetEntityId: entityId, requiresMasterAttention: false }
1002
+ return { reply: '', replyBubbles: [], withheld: true, targetEntityId: entityId, requiresMasterAttention: false }
938
1003
  }
939
1004
  const rawReply = output.replyText?.trim() ?? ''
940
1005
  const bubbles = rawReply.split( /\n{2,}/ ).map( b => b.trim() ).filter( Boolean )
@@ -947,13 +1012,13 @@ export class AuditionEngine extends BaseSenseEngine {
947
1012
  // a safety timeout, then tear the transient facet down.
948
1013
  this._outreachInFlight.add( entityId )
949
1014
  try {
950
- const bubbles = await new Promise<string[]>( resolve => {
1015
+ const authored = await new Promise<OutreachResult>( resolve => {
951
1016
  let settled = false
952
1017
  let unsub: () => void = () => {}
953
1018
  let timer: ReturnType<typeof setTimeout>
954
- const done = ( b: string[] ): void => { if( settled ) return; settled = true; clearTimeout( timer ); unsub(); resolve( b ) }
1019
+ const done = ( r: OutreachResult ): void => { if( settled ) return; settled = true; clearTimeout( timer ); unsub(); resolve( r ) }
955
1020
  timer = setTimeout(
956
- () => { logger.warn(`[audition-engine] outreach authoring timed out for ${ entityId }`); done( [] ) },
1021
+ () => { logger.warn(`[audition-engine] outreach authoring timed out for ${ entityId }`); done( { bubbles: [] } ) },
957
1022
  60_000, // generous: the facet LLM authors in ~8–18s
958
1023
  )
959
1024
  // ONLY this report's decision. Sharing a live conversation facet means its
@@ -962,20 +1027,29 @@ export class AuditionEngine extends BaseSenseEngine {
962
1027
  // composed it unprompted — and deliver it twice.
963
1028
  unsub = handle.subscribe( d => {
964
1029
  if( d.respondingToType !== 'outreach') return
965
- done( ( d.decision as ConversationDecision ).replyBubbles ?? [] )
1030
+ const decision = d.decision as ConversationDecision
1031
+ // A facet reasoned and its decision arrived — `answered`, whatever it
1032
+ // holds. That is what separates an empty ANSWER from the empty every
1033
+ // other exit on this method returns, and it is the only place on the
1034
+ // method that can honestly say so.
1035
+ done( {
1036
+ bubbles: decision.replyBubbles ?? [],
1037
+ withheld: decision.withheld === true,
1038
+ answered: true,
1039
+ } )
966
1040
  } )
967
1041
  // The focus rides the REPORT, so a shared conversation facet keeps its own
968
1042
  // standing focus and its next inbound turn resumes untouched.
969
1043
  Promise.resolve( handle.report({ type: 'outreach', payload: { entityId, gist }, focus: outreachFocus }) ).catch( err => {
970
1044
  logger.warn(`[audition-engine] outreach report failed for ${ entityId }: ${ ( err as Error ).message }`)
971
- done( [] )
1045
+ done( { bubbles: [] } )
972
1046
  } )
973
1047
  } )
974
1048
 
975
1049
  // Only tear down what we opened. Destroying a borrowed conversation facet
976
1050
  // would end the conversation as a side effect of speaking in it.
977
1051
  if( !openThread ) handle.destroy()
978
- return bubbles
1052
+ return authored
979
1053
  }
980
1054
  catch( err ){
981
1055
  // A facet that throws is a pass that produced no words — the same outcome as
@@ -983,7 +1057,7 @@ export class AuditionEngine extends BaseSenseEngine {
983
1057
  // author". Letting it escape would reject inside MotorSchemaExecutor's
984
1058
  // fire-and-forget authoring chain instead.
985
1059
  logger.warn(`[audition-engine] outreach authoring failed for ${ entityId }: ${ ( err as Error ).message }`)
986
- return []
1060
+ return { bubbles: [] }
987
1061
  }
988
1062
  // finally{} on every path — the timeout resolves empty rather than throwing, but
989
1063
  // a destroy() or report() that throws must not leave this person permanently
@@ -1256,6 +1330,13 @@ export class AuditionEngine extends BaseSenseEngine {
1256
1330
  )
1257
1331
  handoff({
1258
1332
  kind: 'undertaking',
1333
+ // The commitment in the mind's own words. A contact is one KIND of
1334
+ // promise, not the only kind the tract can carry — see
1335
+ // UndertakingHandoff. This producer only makes contact-shaped ones,
1336
+ // because a facet declares an outward intent by naming a
1337
+ // communicate action; anything else it means to follow through on
1338
+ // goes through [GOALS_NEW], which the format already asks for.
1339
+ what: `reach ${ intent.target }`,
1259
1340
  target: intent.target,
1260
1341
  reasoning: intent.reasoning ?? '',
1261
1342
  ...( intent.gist ? { gist: intent.gist } : {} ),
@@ -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,7 +99,7 @@ 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
@@ -75,9 +118,24 @@ export interface TextMessage {
75
118
  * false — an unknown room is not known to be public.
76
119
  */
77
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
78
136
  }
79
137
 
80
- export interface VoiceChunk {
138
+ export interface VoiceChunk extends SensorySignal {
81
139
  kind: 'voice'
82
140
  entityId: string
83
141
  threadId: string
@@ -86,14 +144,14 @@ export interface VoiceChunk {
86
144
  }
87
145
 
88
146
  // Vision (stub)
89
- export interface ImageFrame {
147
+ export interface ImageFrame extends SensorySignal {
90
148
  kind: 'image'
91
149
  entityId: string
92
150
  data: Buffer
93
151
  mimeType: string
94
152
  }
95
153
 
96
- export interface VideoSegment {
154
+ export interface VideoSegment extends SensorySignal {
97
155
  kind: 'video'
98
156
  entityId: string
99
157
  frames: ImageFrame[]
@@ -101,41 +159,41 @@ export interface VideoSegment {
101
159
  }
102
160
 
103
161
  // Somatosensation (stub)
104
- export interface WebhookEvent {
162
+ export interface WebhookEvent extends SensorySignal {
105
163
  kind: 'webhook'
106
164
  source: string
107
165
  payload: unknown
108
166
  headers: Record<string, string>
109
167
  }
110
168
 
111
- export interface SystemSignal {
169
+ export interface SystemSignal extends SensorySignal {
112
170
  kind: 'system'
113
171
  signal: string
114
172
  data: unknown
115
173
  }
116
174
 
117
175
  // Olfaction (stub)
118
- export interface AmbientMetric {
176
+ export interface AmbientMetric extends SensorySignal {
119
177
  kind: 'ambient'
120
178
  metricKey: string
121
179
  value: number
122
180
  trend: 'rising' | 'falling' | 'stable'
123
181
  }
124
182
 
125
- export interface BackgroundSignal {
183
+ export interface BackgroundSignal extends SensorySignal {
126
184
  kind: 'background'
127
185
  category: string
128
186
  data: unknown
129
187
  }
130
188
 
131
189
  // Gustation (stub)
132
- export interface InternalEvaluation {
190
+ export interface InternalEvaluation extends SensorySignal {
133
191
  kind: 'self-eval'
134
192
  context: string
135
193
  trigger: string
136
194
  }
137
195
 
138
- export interface SelfAssessmentTrigger {
196
+ export interface SelfAssessmentTrigger extends SensorySignal {
139
197
  kind: 'assessment'
140
198
  goalId?: string
141
199
  checkType: string
@@ -171,7 +229,7 @@ export type SensoryInput =
171
229
  export interface SenseEngine extends CognitiveEngine {
172
230
  readonly domain: SenseDomain
173
231
  attachBus( bus: CognitiveBus ): void
174
- ingest( input: SensoryInput ): Promise<void>
232
+ sense( input: SensoryInput ): Promise<void>
175
233
  }
176
234
 
177
235
  // ── Re-exports ────────────────────────────────────────────────