@mindot/will 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +15 -11
  2. package/dist/index.d.ts +188 -141
  3. package/dist/index.js +16660 -15206
  4. package/dist/index.js.map +1 -1
  5. package/dist/{channels → surface/channels}/discord.d.ts +50 -3
  6. package/dist/{channels → surface/channels}/discord.js +102 -7
  7. package/dist/surface/channels/discord.js.map +1 -0
  8. package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
  9. package/dist/{channels → surface/channels}/whatsapp.js +7 -4
  10. package/dist/surface/channels/whatsapp.js.map +1 -0
  11. package/dist/{cli.js → surface/cli.js} +4406 -2904
  12. package/dist/surface/cli.js.map +1 -0
  13. package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
  14. package/dist/{mcp → surface/mcp}/effectors.js +10 -4
  15. package/dist/surface/mcp/effectors.js.map +1 -0
  16. package/dist/{will-DbDj_TEH.d.ts → will-evj9_vrd.d.ts} +5699 -4854
  17. package/package.json +11 -12
  18. package/src/cognition/agency/consequence.ts +237 -3
  19. package/src/cognition/agency/conversation.aim.ts +32 -0
  20. package/src/cognition/agency/engines/action.selector.ts +59 -3
  21. package/src/cognition/agency/engines/affordance.synthesizer.ts +114 -10
  22. package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
  23. package/src/cognition/agency/engines/motor.schema.executor.ts +362 -27
  24. package/src/cognition/agency/engines/reafference.engine.ts +43 -4
  25. package/src/cognition/agency/execution.primitives.ts +17 -4
  26. package/src/cognition/agency/reconcile.learning.ts +5 -4
  27. package/src/cognition/agency/schemas/innate.ts +96 -1
  28. package/src/cognition/agency/schemas/repertoire.ts +19 -1
  29. package/src/cognition/agency/selection.scoring.ts +16 -0
  30. package/src/cognition/agency/settlement.ts +203 -0
  31. package/src/cognition/agency/types.ts +38 -1
  32. package/src/cognition/config.mirror.entities.ts +0 -1
  33. package/src/cognition/event.schemas.ts +0 -8
  34. package/src/cognition/faculties/affective.blender.ts +21 -4
  35. package/src/cognition/faculties/circadian.oscillator.ts +36 -7
  36. package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
  37. package/src/cognition/faculties/executive.engine/context.ts +41 -35
  38. package/src/cognition/faculties/executive.engine/engine.ts +270 -113
  39. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +126 -59
  40. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +10 -0
  41. package/src/cognition/faculties/executive.engine/parser.ts +100 -38
  42. package/src/cognition/faculties/executive.engine/prompt.factory.ts +168 -23
  43. package/src/cognition/faculties/executive.engine/types.ts +18 -15
  44. package/src/cognition/faculties/exteroception.ts +99 -98
  45. package/src/cognition/faculties/goal.manager.ts +50 -2
  46. package/src/cognition/faculties/known.entity.tracker.ts +22 -3
  47. package/src/cognition/faculties/planning.engine/engine.ts +4 -4
  48. package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
  49. package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
  50. package/src/cognition/faculties/working.memory.ts +6 -1
  51. package/src/cognition/instruction.handler.ts +1 -1
  52. package/src/cognition/memory/vector.embedder.ts +1 -3
  53. package/src/cognition/percept.entity.ts +122 -0
  54. package/src/cognition/sense.boundary.ts +176 -0
  55. package/src/cognition/senses/audition.engine/engine.ts +97 -16
  56. package/src/cognition/senses/base.sense.engine.ts +104 -6
  57. package/src/cognition/senses/index.ts +71 -13
  58. package/src/cognition/senses/provenance.ts +128 -0
  59. package/src/cognition/senses/somatosensation.engine.ts +117 -9
  60. package/src/cognition/social.identity.ts +23 -0
  61. package/src/cognition/utilities/token.tracker.ts +36 -5
  62. package/src/core/orchestrator.ts +13 -0
  63. package/src/index.ts +39 -5
  64. package/src/llm/gate.ts +48 -0
  65. package/src/llm/index.ts +102 -25
  66. package/src/llm/wire.contracts.ts +38 -0
  67. package/src/pma/index.ts +1 -1
  68. package/src/stem/index.ts +92 -21
  69. package/src/stem/mind.ts +26 -2
  70. package/src/stem/policy/arbiter.ts +10 -0
  71. package/src/stem/profiles/built-in.ts +7 -0
  72. package/src/{profiles → stem/profiles}/companion.ts +1 -1
  73. package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
  74. package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
  75. package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
  76. package/src/{profiles → stem/profiles}/index.ts +1 -1
  77. package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
  78. package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
  79. package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
  80. package/src/stem/tracts/effector/types.ts +110 -0
  81. package/src/stem/tracts/effector.controller.ts +112 -401
  82. package/src/stem/tracts/outbox.controller.ts +30 -17
  83. package/src/stem/tracts/sensory.controller.ts +6 -6
  84. package/src/stem/tracts/session.logger.ts +6 -1
  85. package/src/stem/tracts/transport/types.ts +20 -1
  86. package/src/stem/tracts/transport.controller.ts +33 -9
  87. package/src/{channels → surface/channels}/discord.ts +224 -7
  88. package/src/{channels → surface/channels}/roster.ts +1 -1
  89. package/src/{channels → surface/channels}/types.ts +1 -1
  90. package/src/{channels → surface/channels}/whatsapp.ts +9 -6
  91. package/src/{cli.ts → surface/cli.ts} +6 -6
  92. package/src/{host → surface/host}/boot.ts +3 -3
  93. package/src/{host → surface/host}/utterances.ts +2 -2
  94. package/src/{mcp → surface/mcp}/effectors.ts +19 -6
  95. package/src/{mcp → surface/mcp}/server.ts +19 -5
  96. package/src/{sdk → surface/sdk}/will.ts +156 -28
  97. package/src/{serve → surface/serve}/server.ts +11 -4
  98. package/src/types.ts +24 -3
  99. package/dist/channels/discord.js.map +0 -1
  100. package/dist/channels/whatsapp.js.map +0 -1
  101. package/dist/cli.js.map +0 -1
  102. package/dist/mcp/effectors.js.map +0 -1
  103. package/src/extensions/livestream.ext.ts +0 -570
  104. package/src/extensions/time.ext.ts +0 -339
  105. package/src/profiles/built-in.ts +0 -7
  106. package/src/runners/coherence.runner.ts +0 -49
  107. package/src/runners/outreach.runner.ts +0 -155
  108. package/src/runners/social.runner.ts +0 -179
  109. package/src/runners/thin-shim.runner.ts +0 -205
  110. /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
package/src/llm/index.ts CHANGED
@@ -13,7 +13,7 @@ import type {
13
13
  import { type ModelRouter, isNullRouter } from '#llm/routing'
14
14
  import { getCompletionRecorder, getCompletionSource } from '#core/completion.recorder'
15
15
  import type { LLMCompletionRecord } from '#core/completion.recorder'
16
- import { withGate } from '#llm/gate'
16
+ import { withGate, gateFor } from '#llm/gate'
17
17
  import { matchConversationFocus, wrapReplyText } from '#llm/wire.contracts'
18
18
 
19
19
  /**
@@ -264,6 +264,48 @@ export interface LLMCallResult {
264
264
  cacheWriteTok?: number
265
265
  }
266
266
 
267
+ /**
268
+ * The usage block on an Anthropic-wire SSE event. Every field is optional
269
+ * because which of them a host populates — and on which event — varies: see
270
+ * `takeUsage` in `_callAnthropicStream`.
271
+ */
272
+ export interface StreamUsage {
273
+ input_tokens?: number
274
+ output_tokens?: number
275
+ cache_read_input_tokens?: number
276
+ cache_creation_input_tokens?: number
277
+ }
278
+
279
+ /** Running token totals accumulated across a stream's usage events. */
280
+ export interface StreamTokens {
281
+ inputTok: number
282
+ outputTok: number
283
+ cacheReadTok: number
284
+ cacheWriteTok: number
285
+ }
286
+
287
+ /**
288
+ * Fold one SSE usage block into the running totals.
289
+ *
290
+ * Which event carries the real numbers is host-specific. Real Anthropic puts
291
+ * the input side on `message_start` and only `output_tokens` on `message_delta`.
292
+ * Z.ai (glm) sends `message_start` with every field zeroed and reports input,
293
+ * output and cache together on the final `message_delta`. Reading input from
294
+ * `message_start` alone recorded 0 input and 0 cache for every GLM call ever
295
+ * made — 441,896 output tokens against 0 input across one 7-hour COO run.
296
+ *
297
+ * So: fold from wherever it arrives, and let a later non-zero reading win. A
298
+ * zero never overwrites a figure already in hand, and a real figure always
299
+ * replaces a placeholder — which makes the fold correct under either ordering.
300
+ */
301
+ export function foldStreamUsage( acc: StreamTokens, u: StreamUsage ): StreamTokens {
302
+ if( u.input_tokens ) acc.inputTok = u.input_tokens
303
+ if( u.output_tokens ) acc.outputTok = u.output_tokens
304
+ if( u.cache_read_input_tokens ) acc.cacheReadTok = u.cache_read_input_tokens
305
+ if( u.cache_creation_input_tokens ) acc.cacheWriteTok = u.cache_creation_input_tokens
306
+ return acc
307
+ }
308
+
267
309
  /**
268
310
  * Cost-attribution metadata for a single LLM call. The same director instance is
269
311
  * shared by the master executive, every facet (conversation/planning/outreach),
@@ -703,11 +745,24 @@ export class LLMDirector {
703
745
  onChunk: ( chunk: string ) => void,
704
746
  temperature?: number,
705
747
  ): Promise<LLMCallResult> {
706
- // First-byte deadline only: a hard timeout on the whole stream would
707
- // truncate a healthy but long generation, so the timer is cleared once
708
- // the response headers arrive.
748
+ // IDLE deadline, not a whole-response one. A hard cap on the total stream
749
+ // would truncate a healthy long generation; no cap at all lets a stalled
750
+ // socket hang forever. A live generation keeps emitting and a dead one goes
751
+ // quiet, so the clock is restarted by every chunk that arrives and fires
752
+ // only after `_timeoutMs` of silence — the first byte included.
753
+ //
754
+ // It used to be cleared the moment the response headers arrived, which left
755
+ // the read loop with no deadline at all. A mid-stream stall then never
756
+ // settled, and the reasoning promise it belonged to stayed in the executive's
757
+ // `_pending` map forever: `hasPendingWork` gated every cycle and the master
758
+ // seat went silent for the full 600-tick prune horizon. Observed live as ten
759
+ // minutes of a mind not deliberating, announced by nothing.
709
760
  const controller = new AbortController()
710
- const timer = setTimeout( () => controller.abort(), this._timeoutMs )
761
+ let timer = setTimeout( () => controller.abort(), this._timeoutMs )
762
+ const restartDeadline = (): void => {
763
+ clearTimeout( timer )
764
+ timer = setTimeout( () => controller.abort(), this._timeoutMs )
765
+ }
711
766
 
712
767
  let res: Response
713
768
  try {
@@ -732,24 +787,23 @@ export class LLMDirector {
732
787
  throw err
733
788
  }
734
789
 
735
- clearTimeout( timer )
736
-
737
- if( !res.ok )
790
+ if( !res.ok ){
791
+ clearTimeout( timer )
738
792
  throw new Error(`Anthropic stream ${res.status}: ${( await res.text() ).slice(0, 300)}`)
793
+ }
739
794
 
740
795
  const reader = res.body!.getReader()
741
796
  const decoder = new TextDecoder()
742
797
  let buffer = ''
743
- let fullText = ''
744
- let inputTok = 0
745
- let outputTok = 0
746
- let cacheReadTok = 0
747
- let cacheWriteTok = 0
798
+ let fullText = ''
799
+ const tokens: StreamTokens = {
800
+ inputTok: 0, outputTok: 0, cacheReadTok: 0, cacheWriteTok: 0 }
748
801
 
749
802
  try {
750
803
  while( true ){
751
804
  const { done, value } = await reader.read()
752
805
  if( done ) break
806
+ restartDeadline()
753
807
  buffer += decoder.decode( value, { stream: true })
754
808
 
755
809
  const lines = buffer.split('\n')
@@ -764,32 +818,52 @@ export class LLMDirector {
764
818
  const ev = JSON.parse( raw ) as {
765
819
  type: string
766
820
  delta?: { type: string; text?: string; stop_reason?: string }
767
- message?: { usage?: { input_tokens: number; cache_read_input_tokens?: number; cache_creation_input_tokens?: number } }
768
- usage?: { output_tokens: number }
821
+ message?: { usage?: StreamUsage }
822
+ usage?: StreamUsage
769
823
  }
770
824
 
771
- if( ev.type === 'message_start' && ev.message?.usage ){
772
- inputTok = ev.message.usage.input_tokens
773
- cacheReadTok = ev.message.usage.cache_read_input_tokens ?? 0
774
- cacheWriteTok = ev.message.usage.cache_creation_input_tokens ?? 0
775
- }
825
+ // Usage arrives in different events depending on the host. Real
826
+ // Anthropic reports the input side up front on `message_start` and
827
+ // only `output_tokens` on `message_delta`. Anthropic-WIRE hosts do
828
+ // not all follow that: Z.ai (glm) sends `message_start` with zeroed
829
+ // placeholders and puts the true figures — input, output AND cache —
830
+ // on the final `message_delta`. Reading the input side from
831
+ // `message_start` alone therefore recorded 0 input and 0 cache for
832
+ // every GLM call ever made, while output logged correctly: 441,896
833
+ // output tokens against 0 input across one 7-hour COO run, which is
834
+ // not a possible shape for a conversation.
835
+ //
836
+ // So take usage wherever it appears and let a later non-zero reading
837
+ // win — a zero never overwrites a figure already in hand, and a real
838
+ // figure always replaces a placeholder.
839
+ if( ev.type === 'message_start' && ev.message?.usage )
840
+ foldStreamUsage( tokens, ev.message.usage )
776
841
  else if( ev.type === 'content_block_delta' && ev.delta?.text ){
777
842
  fullText += ev.delta.text
778
843
  onChunk( ev.delta.text )
779
844
  }
780
- else if( ev.type === 'message_delta' && ev.usage?.output_tokens ){
781
- outputTok = ev.usage.output_tokens
782
- }
845
+ else if( ev.type === 'message_delta' && ev.usage )
846
+ foldStreamUsage( tokens, ev.usage )
783
847
  }
784
848
  catch { /* ignore malformed events */ }
785
849
  }
786
850
  }
787
851
  }
852
+ catch( err ){
853
+ // An abort here is the idle deadline, not a caller cancelling: nothing
854
+ // arrived for `_timeoutMs` while the stream was still open. Naming it is
855
+ // the whole point — a stall that throws is a pass that failed, and a pass
856
+ // that failed settles its promise and frees the seat.
857
+ if( controller.signal.aborted )
858
+ throw new Error(`LLM stream to ${ep.provider} stalled — no data for ${this._timeoutMs}ms`)
859
+ throw err
860
+ }
788
861
  finally {
862
+ clearTimeout( timer )
789
863
  reader.releaseLock()
790
864
  }
791
865
 
792
- return { text: fullText, inputTok, outputTok, cacheReadTok, cacheWriteTok }
866
+ return { text: fullText, ...tokens }
793
867
  }
794
868
 
795
869
  /**
@@ -831,7 +905,10 @@ export class LLMDirector {
831
905
  () => ep.wire === 'anthropic'
832
906
  ? this._callAnthropicStream( ep, systemPrompt, userMessage, () => {}, temperature )
833
907
  : this._callProvider( ep, systemPrompt, userMessage, temperature ),
834
- 'executive/direct',
908
+ `executive/direct:${ meta.function }`,
909
+ // A reply to a waiting person takes the reserved lane; everything the
910
+ // mind is doing for itself shares the general one. See `gateFor`.
911
+ gateFor( meta.function ),
835
912
  )
836
913
 
837
914
  // Record token usage + cost into this Will's injected tracker (R4), tagged
@@ -58,6 +58,44 @@ export function wrapReplyText( body: string ): string {
58
58
  return [ REPLY_TEXT_OPEN, body, REPLY_TEXT_CLOSE ].join('\n')
59
59
  }
60
60
 
61
+ /**
62
+ * Every marker that is PROTOCOL rather than content.
63
+ *
64
+ * Named explicitly rather than matched as `[ANYTHING]`, because bracketed text is
65
+ * ordinary in real messages — "[1]", "[see §4.4]", "[REDACTED]" are things a mind
66
+ * may legitimately say, and a greedy strip would eat them.
67
+ */
68
+ export const PROTOCOL_TAGS: readonly string[] = [
69
+ REPLY_TEXT_TAG, NO_MESSAGE_TAG, 'INTROSPECTION', 'NARRATIVE', 'SELF_OBS',
70
+ ]
71
+
72
+ /**
73
+ * Remove any protocol marker that survived inside extracted content.
74
+ *
75
+ * `extractTextBlock` slices from the first `[TAG]` to the first `[/TAG]` after
76
+ * it, so a STRAY second opener inside the body is carried out as content — and
77
+ * the bubble splitter, seeing a line of its own, delivers it as a message.
78
+ *
79
+ * Live: a COO's reply to a technical document went out as four substantive
80
+ * bubbles followed by a fifth reading exactly `[REPLY_TEXT]`. The person got a
81
+ * message whose entire content was the name of the slot it should have filled.
82
+ *
83
+ * These tokens can never be legitimate content — they are the wire, not the
84
+ * words — so stripping them is not censorship of anything the mind meant. A line
85
+ * left empty by the removal is dropped so it cannot become an empty bubble.
86
+ */
87
+ export function stripProtocolMarkers( text: string ): string {
88
+ let out = text
89
+ for( const tag of PROTOCOL_TAGS )
90
+ out = out.split(`[${ tag }]`).join('').split(`[/${ tag }]`).join('')
91
+
92
+ return out
93
+ .split('\n')
94
+ .filter( ( line, i, all ) => line.trim() !== '' || ( i > 0 && i < all.length - 1 && all[ i - 1 ]?.trim() !== '') )
95
+ .join('\n')
96
+ .trim()
97
+ }
98
+
61
99
  // ── Conversation-facet focus — render ↔ match pair ────────────
62
100
  // The AuditionEngine RENDERS these lines into the facet focus; the mock LLM
63
101
  // MATCHES them to detect "this call is a conversation turn" and synthesize a
package/src/pma/index.ts CHANGED
@@ -282,7 +282,7 @@ export class PMADistiller {
282
282
  * @param willId The Will's ID
283
283
  * @param willName The Will's display name
284
284
  * @param state Current simulation state (from stateManager.snapshot())
285
- * @param sessionId Active session ID — recorded as provenance
285
+ * @param sessionId Active session ID — recorded on the snapshot as its origin
286
286
  * @param dataDir Root data dir (defaults to WILL_DATA_DIR env or './data')
287
287
  * @param repertoire The agency competence layer (in-memory manager). When
288
288
  * provided, the Will's learned skills + composite schemas are
package/src/stem/index.ts CHANGED
@@ -38,6 +38,8 @@ import { TransportController } from '#stem/tracts/transport.controller'
38
38
  import { InboundQueue } from '#stem/tracts/inbound.queue'
39
39
  import type { ExternalTransport } from '#stem/tracts/transport'
40
40
  import { effectorController } from '#stem/tracts/effector.controller'
41
+ import type { EffectorAck } from '#stem/tracts/effector/types'
42
+ import type { PolicyArbiter } from '#stem/policy/arbiter'
41
43
  import { externalSchemas } from '#agency/schemas/external'
42
44
  import { inFlightOnRestore } from '#agency/restart'
43
45
  import { buildEngineConfigEntities } from '#cognition/config.mirror.entities'
@@ -575,16 +577,30 @@ export class WillStem {
575
577
  ? `${offlineMins} minutes`
576
578
  : `${Math.round( offlineMins / 60 )} hours`
577
579
 
578
- instance.simulation.stateManager.setEntity({
579
- id: 'percept-wake-event',
580
- type: 'percept',
581
- createdAt: Date.now(),
582
- updatedAt: Date.now(),
583
- metadata: {
584
- category: 'system',
585
- summary: `I was offline for ${duration}. I am now online again.`,
586
- salience: 0.75,
587
- source: 'system',
580
+ // Through the SENSE DOOR, not a hand-written percept (SIGNAL_BOUNDARY P1).
581
+ //
582
+ // This used to build a `percept` entity here, by hand, and every bug it
583
+ // had came from that: no tick (so it never expired, and told the executive
584
+ // "I was offline for 3 hours" for the rest of the mind's life), and no
585
+ // provenance (so `action.selector`'s rupture gate excluded it exactly as
586
+ // it excludes the mind's own echo — a mind returning after hours away
587
+ // could not be ruptured by noticing).
588
+ //
589
+ // Both were fixed by hand in P0 step 2. This removes the hand: waking is
590
+ // the world touching the mind, so it arrives as a `SystemSignal` through
591
+ // somatosensation like any other touch, and the sense supplies the tick,
592
+ // the trace, and the stamp because that is what a sense does. The wake
593
+ // event stops being special.
594
+ //
595
+ // Fire-and-forget: `ingest` is async and the resume path is not. The
596
+ // signal reaches state on the sense's own timing, exactly as an inbound
597
+ // message does.
598
+ void instance.cognition.somatosensationEngine.sense({
599
+ kind: 'system',
600
+ signal: 'WAKE',
601
+ provenance: 'exafferent', // time passed; nothing I did caused it
602
+ data: {
603
+ summary: `I was offline for ${duration}. I am now online again.`,
588
604
  offlineMs,
589
605
  },
590
606
  })
@@ -731,7 +747,35 @@ export class WillStem {
731
747
  * Resume the Will after loading to start ticking with the seeded state.
732
748
  */
733
749
  loadPMA( id: string, pma: PMASnapshot ): void {
734
- this._pma.load( id, this._get( id ), pma )
750
+ const instance = this._get( id )
751
+ this._pma.load( id, instance, pma )
752
+
753
+ // A MIND THAT WAKES SHOULD KNOW IT WAS AWAY (SIGNAL_BOUNDARY P1, follow-up).
754
+ //
755
+ // `resumeWill` has always raised a wake event, gated on `instance.pausedAt`
756
+ // — and `pausedAt` is set ONLY by `pauseWill()`, an in-session pause. The
757
+ // hibernate→wake lifecycle never touches it: `Will.wake` calls
758
+ // `createWill( config, startPaused: true )`, which sets `status = 'paused'`
759
+ // but leaves `pausedAt` null, and `resumeWill` then skips the whole block.
760
+ //
761
+ // So the one lifecycle every deployed Will actually uses was the one that
762
+ // never fired. Found by running a live Will after P1 routed the wake through
763
+ // the sense door: the door was correct and nothing walked through it. The
764
+ // unit test passed because it set `pausedAt` by hand — which is precisely
765
+ // the kind of test that agrees with you instead of checking.
766
+ //
767
+ // The PMA has carried the answer all along: `distilledAt` is when this mind
768
+ // was distilled, i.e. when it stopped. Loading a PMA IS the statement "this
769
+ // mind existed before and has been away since then", so this is where the
770
+ // fact belongs. A Will born fresh never calls this and is never told it
771
+ // woke, which is right — it was not away, it did not exist.
772
+ //
773
+ // Guarded: a clock skew or a hand-edited artifact must not tell a mind it
774
+ // has been away for negative time, or since 1970.
775
+ const distilled = pma.distilledAt
776
+ if( typeof distilled === 'number' && Number.isFinite( distilled )
777
+ && distilled > 0 && distilled <= Date.now() )
778
+ instance.pausedAt = new Date( distilled )
735
779
  }
736
780
 
737
781
  // ── Replay ─────────────────────────────────────────────────
@@ -871,15 +915,20 @@ export class WillStem {
871
915
  * `agency.intent` id). Reconciles it into an `agency.outcome` the ReafferenceEngine
872
916
  * consumes — learning the result, freeing the intent, and advancing the plan it
873
917
  * served (if any). See effectorController.confirmExecution.
918
+ *
919
+ * Takes the full `EffectorAck`. It used to declare its own narrower shape —
920
+ * `{ success, description, metrics? }` — which P2 left behind: the controller
921
+ * has routed `observation` into a reafferent percept since #150, but a host
922
+ * driving the stem DIRECTLY could not typecheck one, so the facts half of the
923
+ * ack was reachable only through the SDK facade. A type that silently withholds
924
+ * half a boundary from the hosts most likely to need it — a robot control
925
+ * layer, a game loop, anything not using the facade — is the same defect class
926
+ * as a comment that outlives its claim, with a compiler enforcing it.
874
927
  */
875
928
  confirmEffectorExecution(
876
929
  id: string,
877
930
  invocationId: string,
878
- result: {
879
- success: boolean
880
- description: string
881
- metrics?: Record<string, number>
882
- },
931
+ result: EffectorAck,
883
932
  ): void {
884
933
  this._effector.confirmExecution( this._get( id ), invocationId, result )
885
934
  }
@@ -894,6 +943,28 @@ export class WillStem {
894
943
  this._effector.resolveEscalation( this._get( id ), invocationId, approved )
895
944
  }
896
945
 
946
+
947
+ /**
948
+ * Install the Policy Decision Point consulted before every host-owned
949
+ * effector invocation is handed to the world (POLICY_REAFFERENCE P0).
950
+ * Passing `null` restores the no-op default — a stem with no arbiter
951
+ * installed runs byte-identical to one built before the policy seam existed.
952
+ *
953
+ * SCOPE: one arbiter per `WillStem`, not per Will — `effectorController` is a
954
+ * single instance shared by every Will this stem hosts (`setAllowed`,
955
+ * `resolveEscalation` etc. take a resolved instance; this does not, because
956
+ * there is only one). `Will.create()` / `Will.wake()` each allocate a fresh
957
+ * `WillStem`, so on that (recommended) path this is per-Will in practice. A
958
+ * host running several Wills on ONE shared `WillStem` — e.g. a multi-tenant
959
+ * service holding many users' Wills in one process — installs ONE arbiter
960
+ * for all of them; branch on `invocation.willId` inside `evaluate()` if
961
+ * that's your host, the same way `PolicyVerdictRecord`/`PolicyVerdictSource`
962
+ * are already keyed per-Will for recording and replay.
963
+ */
964
+ setArbiter( arbiter: PolicyArbiter | null ): void {
965
+ this._effector.setArbiter( arbiter )
966
+ }
967
+
897
968
  // ── Messaging / outbox (11.1) ────────────────────────────────────────────
898
969
  // Delegates to OutboxController (R5-c). `_get(id)` validates the Will exists
899
970
  // and supplies the WillInstance; the outbox ops touch only instance fields.
@@ -952,8 +1023,8 @@ export class WillStem {
952
1023
  * @param id Will ID
953
1024
  * @param input TextMessage — `{ kind: 'text', entityId, threadId, content, speakerName? }`
954
1025
  */
955
- async ingestText( id: string, input: TextMessage ): Promise<void> {
956
- await this._sensory.ingestText( this._get( id ), input )
1026
+ async senseText( id: string, input: TextMessage ): Promise<void> {
1027
+ await this._sensory.senseText( this._get( id ), input )
957
1028
  }
958
1029
 
959
1030
  /**
@@ -1005,10 +1076,10 @@ export class WillStem {
1005
1076
  /**
1006
1077
  * Route a raw SensoryInput to the appropriate sense engine by domain.
1007
1078
  * Used by the debug `POST /senses/:domain/ingest` route.
1008
- * Audition inputs are gated by the 'listen' effector like ingestText().
1079
+ * Audition inputs are gated by the 'listen' effector like senseText().
1009
1080
  */
1010
- async ingestSensory( id: string, domain: string, input: SensoryInput ): Promise<void> {
1011
- await this._sensory.ingestSensory( this._get( id ), domain, input )
1081
+ async senseSignal( id: string, domain: string, input: SensoryInput ): Promise<void> {
1082
+ await this._sensory.senseSignal( this._get( id ), domain, input )
1012
1083
  }
1013
1084
 
1014
1085
  listWills(): WillSummary[] {
package/src/stem/mind.ts CHANGED
@@ -26,14 +26,14 @@ import { OutboxWriter } from '#stem/tracts/outbox.writer'
26
26
  import { ExecutiveSummarizer } from '#llm/summarizer'
27
27
  import type { LLMProvider } from '#llm/index'
28
28
  import { TableRouter, chainRouters, type ModelRouter, type RoutingRule } from '#llm/routing'
29
- import { resolveProfile } from '#profiles/index'
29
+ import { resolveProfile } from '#stem/profiles/index'
30
30
  import { DefaultVectorMemoryAdapter } from '#memory/vector.adapter'
31
31
  import { OpenAICompatibleEmbedder, MockEmbedder } from '#memory/vector.embedder'
32
32
  import type { VectorMemoryAdapter } from '#memory/vector.adapter'
33
33
  import type { ExternalTransport } from '#stem/tracts/transport'
34
34
  import type { Cognition, OutboxMessage } from '#types'
35
35
  import type { StorageAdapter } from '#core/abstracts'
36
- import '#profiles/built-in'
36
+ import '#stem/profiles/built-in'
37
37
 
38
38
  // ── Agency pipeline (new action system) ──────────────────────
39
39
  import { ProactiveCommunicator } from '#agency/proactive.communicator'
@@ -100,6 +100,7 @@ import {
100
100
  GustationEngine
101
101
  } from '#cognition/index'
102
102
  import { buildEngineConfigEntities, mergeEngineConfig, EngineConfigEntity } from '#cognition/config.mirror.entities'
103
+ import { endogenousTypes } from '#cognition/sense.boundary'
103
104
  import { mergeIdentity, composeIdentityPrompt, WILL_CORE_PREAMBLE } from '#cognition/identity.entity'
104
105
  import {
105
106
  isReferentId, readAliases, handlesOf, defaultHandle,
@@ -1064,6 +1065,23 @@ function _constructCognition(
1064
1065
  // buildExecutiveContext (already vector-backed via the consolidator).
1065
1066
  auditionEngine.attachMemorySink( entity => simulation.stateManager.setEntity( entity ) )
1066
1067
 
1068
+ // Every sense lays down a percept trace (SIGNAL_BOUNDARY P0). Wired for all
1069
+ // five, not just the implemented one, because that is the contract a host is
1070
+ // owed: implement a sense and what it senses reaches the faculties that read
1071
+ // percepts, rather than a bus event three subscribers glance at for one tick.
1072
+ //
1073
+ // The tick is a getter, not a value — a sense is ingest-driven and off-tick,
1074
+ // so it has no `react()` to be handed one in, and a captured number would
1075
+ // stamp every percept with whenever assembly ran.
1076
+ //
1077
+ // AuditionEngine opts out (`tracesPercepts = false`); it still gets the wiring
1078
+ // so turning it on is a one-word change when the measurement is done.
1079
+ for( const sense of [ auditionEngine, visionEngine, somatosensationEngine, olfactionEngine, gustationEngine ] )
1080
+ sense.attachPerceptTrace(
1081
+ entity => simulation.stateManager.setEntity( entity ),
1082
+ () => simulation.clock.currentTick,
1083
+ )
1084
+
1067
1085
  // Referent → address + room. Closes over the state manager because the writer
1068
1086
  // is deliberately stateless; this is the one seam both send paths cross, so the
1069
1087
  // translation happens once rather than in each of them.
@@ -1307,6 +1325,12 @@ function _registerEngines( simulation: DefaultSimulation, cognition: Cognition,
1307
1325
  activeEngines
1308
1326
  .sort( ( a: any, b: any ) => a.priority - b.priority )
1309
1327
  .forEach( e => simulation.addEngine( e ) )
1328
+
1329
+ // Where this mind ends. Read from the LIVE engine list, not a copy, so an
1330
+ // engine a host registers after assembly is inside the boundary too — its
1331
+ // declared `writes` are machinery, and machinery is not perceived.
1332
+ cognition.exteroception.attachBoundary(
1333
+ () => endogenousTypes( simulation.orchestrator.engines ) )
1310
1334
  }
1311
1335
 
1312
1336
  /**
@@ -98,6 +98,16 @@ export interface PolicyInvocation {
98
98
  schema: string
99
99
  parameters: Record<string, unknown>
100
100
  targetEntityId?: string
101
+ /**
102
+ * The addresses this host knows `targetEntityId` by (e.g. `discord:123…`) —
103
+ * the same enrichment a host effector handler already receives on `ctx`
104
+ * (`EffectorHandler`'s `ctx.targetAddresses`, `surface/sdk/will.ts`). The
105
+ * arbiter sits at the identical boundary, one step earlier in the same flow,
106
+ * so withholding it here bought no extra privacy — a handler downstream of an
107
+ * ALLOW already sees it. Absent when the invocation binds no target or the
108
+ * entity has no known address.
109
+ */
110
+ targetAddresses?: readonly string[]
101
111
  /** The ability's declared meaning, as given by the host at wiring time. */
102
112
  description?: string
103
113
  tick: number
@@ -0,0 +1,7 @@
1
+ // Side-effect import — registers all built-in world profiles.
2
+ // Import this once at application entry (mind.ts) before resolveProfile is called.
3
+ import '#stem/profiles/customer-service'
4
+ import '#stem/profiles/companion'
5
+ import '#stem/profiles/smart-home'
6
+ import '#stem/profiles/game-npc'
7
+ import '#stem/profiles/company-brain'
@@ -1,4 +1,4 @@
1
- import { registerProfile } from '#profiles/index'
1
+ import { registerProfile } from '#stem/profiles/index'
2
2
 
3
3
  registerProfile({
4
4
  id: 'companion',
@@ -1,4 +1,4 @@
1
- import { registerProfile } from '#profiles/index'
1
+ import { registerProfile } from '#stem/profiles/index'
2
2
 
3
3
  registerProfile({
4
4
  id: 'company-brain',
@@ -1,4 +1,4 @@
1
- import { registerProfile } from '#profiles/index'
1
+ import { registerProfile } from '#stem/profiles/index'
2
2
 
3
3
  registerProfile({
4
4
  id: 'customer-service',
@@ -1,4 +1,4 @@
1
- import { registerProfile } from '#profiles/index'
1
+ import { registerProfile } from '#stem/profiles/index'
2
2
 
3
3
  registerProfile({
4
4
  id: 'game-npc',
@@ -1,5 +1,5 @@
1
1
  // ─────────────────────────────────────────────────────────────
2
- // src/profiles/index.ts — World profile registry
2
+ // src/stem/profiles/index.ts — World profile registry
3
3
  // ─────────────────────────────────────────────────────────────
4
4
  //
5
5
  // A world profile is a named configuration preset that:
@@ -1,4 +1,4 @@
1
- import { registerProfile } from '#profiles/index'
1
+ import { registerProfile } from '#stem/profiles/index'
2
2
 
3
3
  registerProfile({
4
4
  id: 'smart-home',