@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
@@ -18,6 +18,7 @@
18
18
  */
19
19
 
20
20
  import { logger } from '#core/logger'
21
+ import { readAliases, canonicalOf } from '#cognition/social.identity'
21
22
  import type { SessionLogger } from '#stem/tracts/session.logger'
22
23
  import type {
23
24
  Tick,
@@ -28,6 +29,9 @@ import type {
28
29
  EntityInput
29
30
  } from '#core/types'
30
31
  import { AsyncEngine } from '#core/async.engine'
32
+ import {
33
+ actionRecordEntity, staleActionRecordIds, type ActionStatus,
34
+ } from '#faculties/executive.engine/action.record'
31
35
  import type { IntermediateStream } from '#core/async.engine'
32
36
  import type { WorkingMemory } from '#faculties/working.memory'
33
37
  import type { GoalManager, GoalState } from '#faculties/goal.manager'
@@ -84,8 +88,6 @@ import type { EngineResult } from '#core/orchestrator'
84
88
  import type { Duration } from '#core/types'
85
89
  import { wallClock } from '#core/wall.clock'
86
90
 
87
- // Re-export for compatibility
88
- export { ExecutiveFacet, type ExecutiveFacetHandle }
89
91
 
90
92
  /**
91
93
  * How hard a reasoning facet pulls on attention, as an `attention.demand` urgency.
@@ -199,13 +201,39 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
199
201
  private readonly _facetSupervisor = new FacetSupervisor()
200
202
 
201
203
  /**
202
- * Who each live facet is engaged with, learned from `executive.facet.sync`.
203
- * Keyed by facetId; the last sync wins. Rendered into the master's own prompt so
204
- * the singular seat can reason across its conversations "as if they were sitting
205
- * at the same table" — which it cannot do while it only knows facet numbers.
206
- * Stale entries age out on read (see _activeConversations).
204
+ * Who each live facet is engaged with AND what it concluded there, learned from
205
+ * `executive.facet.sync`. Keyed by facetId; the last sync wins. Rendered into
206
+ * the master's own prompt so the singular seat can reason across its
207
+ * conversations "as if they were sitting at the same table" — which it cannot
208
+ * do while it only knows facet numbers. Stale entries age out on read (see
209
+ * _activeConversations).
210
+ *
211
+ * `concluded` IS THE RETURN LEG, and it did not exist. `executive.facet.sync`
212
+ * has always carried the facet's full `reasoning`; this handler received it and
213
+ * dropped it on the floor. So the sync was one-way: the master's thinking flows
214
+ * DOWN to facets as "What I've Been Turning Over", while what a facet worked out
215
+ * came back as identity and a salience spike — that attention had been engaged,
216
+ * and with whom, never what it concluded.
217
+ *
218
+ * A facet is the same mind with a focus, not a subordinate reporting in. A mind
219
+ * that cannot read back its own thinking from where its attention has been is
220
+ * split, and the prompt comment two files over already named the consequence:
221
+ * "telling one person it has contacted another when it has not".
222
+ *
223
+ * It is kept HERE, on the engine, rather than as a percept, because a percept is
224
+ * swept after 2 ticks and a working-memory item decays below the retrieval
225
+ * threshold in about 9 — while the master's own interval is 15. Anything routed
226
+ * that way is usually gone before the master next reads. This survives to the
227
+ * next cycle by construction.
207
228
  */
208
- private _facetSubjects = new Map<string, { entityId: string; name?: string; tick: number }>()
229
+ private _facetSubjects = new Map<string, {
230
+ entityId: string
231
+ name?: string
232
+ tick: number
233
+ concluded?: string
234
+ /** Commitments the facet declared toward a THIRD party while attending here. */
235
+ promised?: Array<{ what: string; target?: string; gist?: string; tick: number }>
236
+ }>()
209
237
 
210
238
  // ── Cognitive models ───────────────────────────────────────
211
239
  private readonly _model = new GenerativeModel()
@@ -247,6 +275,11 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
247
275
  // See EscalationBuffer for the full rationale (R5-g-2).
248
276
  private readonly _escalations = new EscalationBuffer()
249
277
 
278
+ /** First tick of an unbroken `hasPendingWork` suppression; null when the seat is free. */
279
+ private _mutedSince: Tick | null = null
280
+ /** Whether this suppression episode has already been reported. */
281
+ private _mutedWarned = false
282
+
250
283
  /** Set/clear the chunk broadcaster (called by WillManager when SSE clients connect). */
251
284
  setChunkBroadcaster( fn: (( chunk: string ) => void) | null ): void {
252
285
  this._chunkBroadcaster = fn
@@ -446,7 +479,7 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
446
479
  // attachments. The supervisor owns the registry + attention budget and
447
480
  // performs the throw-checks on bus / director / state ref.
448
481
  return this._facetSupervisor.spawn( {
449
- ...( key ? { key } : {} ),
482
+ ...( key ? { key: this._canonicalKey( key ) } : {} ),
450
483
  bus: this._bus,
451
484
  llmDirector: this._llmDirector,
452
485
  stateRef: this._lastStateRef,
@@ -469,7 +502,45 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
469
502
  * See FacetSupervisor.handleFor.
470
503
  */
471
504
  facetFor( key: string ): ExecutiveFacetHandle | undefined {
472
- return this._facetSupervisor.handleFor( key )
505
+ // Tolerant of BOTH id spaces, canonical first.
506
+ //
507
+ // A facet registered before its anchor was visible is still that person's
508
+ // open thread, and answering "nobody home" would open a second one beside it
509
+ // — the exact failure this key exists to prevent. Two ways that happens: a
510
+ // first message racing the tracker's mint, and a later name-merge that
511
+ // re-points one anchor onto another (KNOWN_ENTITY Phase 5, which is
512
+ // deliberately revisable and never destructively re-keys).
513
+ return this._facetSupervisor.handleFor( this._canonicalKey( key ) )
514
+ ?? this._facetSupervisor.handleFor( key )
515
+ }
516
+
517
+ /**
518
+ * A facet key in the PERSON's id space, not the address's.
519
+ *
520
+ * `FacetSpawnDeps.key` is `<role>:<entityId>`, and the two sides of that
521
+ * contract disagreed about which space `entityId` lives in. Audition spawns a
522
+ * conversation facet keyed by `percept.speakerEntityId` — the transport address
523
+ * the message arrived on (`discord:1019…`) — while `authorOutreach` asks for
524
+ * `conversation:<anchor>` (`ke:1sqlkux`), because the executive resolves a
525
+ * person to their anchor before willing anything at them.
526
+ *
527
+ * So the dedup this key exists for never fired for a master-willed outreach.
528
+ * Every one spawned a transient rival facet on someone the mind was already
529
+ * talking to — which is what the key was added to prevent, and what its own
530
+ * comment claims it does. Observed live: a reply and an unprompted second
531
+ * answer to the same question 27 seconds apart, the second composed by a facet
532
+ * that could not see the first.
533
+ *
534
+ * Safe to resolve here because KnownEntityTracker mints the anchor on FIRST
535
+ * sight of an address, deterministically from it (R2), so a conversation is
536
+ * keyed the same way from its first message onward.
537
+ */
538
+ private _canonicalKey( key: string ): string {
539
+ const cut = key.indexOf(':')
540
+ if( cut < 0 || !this._lastStateRef ) return key
541
+ const id = key.slice( cut + 1 )
542
+ const canon = canonicalOf( readAliases( this._lastStateRef.entities ), id )
543
+ return canon === id ? key : `${ key.slice( 0, cut ) }:${ canon }`
473
544
  }
474
545
 
475
546
  // ── CognitiveEngine interface ──────────────────────────────
@@ -477,6 +548,49 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
477
548
  subscribes(): string[] { return [ '*' ] }
478
549
  publishes(): CognitiveEventSchema[] { return [] }
479
550
 
551
+ /**
552
+ * Fold one resolved act into the mind's record of its own doing.
553
+ *
554
+ * `withheld` is kept distinct from `failed` deliberately: the mind formed the
555
+ * act and chose not to complete it, which is a judgement, not an inability.
556
+ * Collapsing them is the #123 mistake — a COO learning it was bad at speaking
557
+ * from the times it decided not to speak.
558
+ */
559
+ private _recordAction( event: CognitiveEvent ): StateCommands | void {
560
+ const p = event.payload as {
561
+ actionType?: unknown; success?: unknown; targetEntityId?: unknown
562
+ description?: unknown; planId?: unknown; tick?: unknown
563
+ } | undefined
564
+ const type = typeof p?.actionType === 'string' ? p.actionType : undefined
565
+ if( !type ) return
566
+
567
+ const status: ActionStatus = event.type === 'action.withheld' ? 'withheld'
568
+ : p?.success === false ? 'failed'
569
+ : 'completed'
570
+
571
+ const record = actionRecordEntity({
572
+ type, status,
573
+ tick: typeof p?.tick === 'number' ? p.tick : 0,
574
+ ...( typeof p?.targetEntityId === 'string' ? { targetEntityId: p.targetEntityId } : {} ),
575
+ // WHOLE. 120 here, 300 at the session log and 700 at the MCP boundary were
576
+ // three unexamined numbers cutting the same string, and the cut was the
577
+ // engine deciding how much of what a host said the mind was allowed to
578
+ // keep. What a host sends is consumed in its entirety; what the engine
579
+ // itself composes it may bound (see `PERCEPT_SUMMARY_CAP`), because there
580
+ // it is not destroying anyone's only copy.
581
+ //
582
+ // Safe to leave unbounded only because P2 split fate from facts: this
583
+ // carries a FATE, which is short by nature. The answer to a lookup leaves
584
+ // through `observation` and reaches the mind as a percept.
585
+ outcome: typeof p?.description === 'string' ? p.description : '',
586
+ ...( typeof p?.planId === 'string' ? { planId: p.planId } : {} ),
587
+ })
588
+
589
+ // Pruning happens in `react`, which has frozen state to prune against; an
590
+ // event handler sees only the event.
591
+ return { set: [ record ] }
592
+ }
593
+
480
594
  snapshot(): Record<string, unknown> {
481
595
  return {
482
596
  bufferSize: this._gatingState.salienceBuffer.length,
@@ -488,6 +602,24 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
488
602
  onCognitiveEvent( event: CognitiveEvent ): StateCommands | void {
489
603
  if( event.sourceEngine === this.name ) return
490
604
 
605
+ // What became of what I did.
606
+ //
607
+ // the outcomes section has existed — section, builder and type —
608
+ // since it was written, fed by `context.ts` scanning `decision.record` for
609
+ // an `actionStatus` that nothing in the engine has ever set. It rendered 0
610
+ // times across every prompt a live COO received. So the mind could see what
611
+ // it had SAID and never what it had DONE, and its only other record of
612
+ // acting is `## Recent Actions`, which is built from the executive's own
613
+ // DECISIONS. A record of acting that contains only intentions cannot
614
+ // distinguish an intention from an act — and one did not, reporting a spec
615
+ // as drafted and sent when it had no effectors and had sent nothing.
616
+ //
617
+ // Written here because these events already arrive here (`subscribes()` is
618
+ // `['*']`) and the returned commands are drained onto state by the
619
+ // orchestrator. See `action.record.ts`.
620
+ if( event.type === 'action.outcome' || event.type === 'action.withheld')
621
+ return this._recordAction( event )
622
+
491
623
  // Spare attention scales the facet allowance within the persona's ceiling.
492
624
  if( event.type === 'attention.state.changed'){
493
625
  const p = event.payload as { freeFraction: number }
@@ -553,6 +685,14 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
553
685
  result.commands.delete = [ ...( result.commands.delete ?? [] ), ...focus.delete ]
554
686
  }
555
687
 
688
+ // Hold as many acts as the prompt shows and no more. Done here rather than
689
+ // in the event handler because pruning needs frozen state to prune against.
690
+ const stale = staleActionRecordIds( state.entities as never )
691
+ if( stale.length > 0 ){
692
+ result.commands ??= {}
693
+ result.commands.delete = [ ...( result.commands.delete ?? [] ), ...stale ]
694
+ }
695
+
556
696
  return result
557
697
  }
558
698
 
@@ -677,6 +817,26 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
677
817
  // Always update counters, even when not activating
678
818
  updateGatingState( this._gatingState, state, tick, result.shouldActivate, result.cleanedBuffer )
679
819
 
820
+ // A reasoning pass that never settles holds this gate shut until the
821
+ // AsyncEngine's stale prune drops it 600 ticks later, and every one of those
822
+ // ticks was silent — the mind simply stopped deliberating and nothing said
823
+ // why. Warn ONCE, at the point a pass has outlived any healthy one; warning
824
+ // every tick would bury the line it exists to surface.
825
+ if( result.reason === 'hasPendingWork'){
826
+ this._mutedSince ??= tick
827
+ // Once per episode, not once at an exact tick: react() is not guaranteed to
828
+ // be called on every tick, and an `=== threshold` test that the clock steps
829
+ // over never fires at all — a silent warning about a silence.
830
+ if( !this._mutedWarned && tick - this._mutedSince >= this._executiveInterval * 3 ){
831
+ this._mutedWarned = true
832
+ logger.warn(
833
+ `[executive] no cycle for ${ tick - this._mutedSince } ticks — a reasoning pass ` +
834
+ `has not settled and is holding the seat (tick=${tick})`
835
+ )
836
+ }
837
+ }
838
+ else { this._mutedSince = null; this._mutedWarned = false }
839
+
680
840
  if( result.shouldActivate )
681
841
  logger.info(`[executive] activating — reason: ${result.reason} (tick=${tick})`)
682
842
 
@@ -991,11 +1151,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
991
1151
  plansCount: executiveOutput.plans?.length ?? 0,
992
1152
  goalsNew: executiveOutput.newGoals ?? [],
993
1153
  goalsAbandon: executiveOutput.goalsToAbandon ?? [],
994
- replies: ( executiveOutput.conversationReplies ?? [] ).map( r => ( {
995
- targetEntityId: r.targetEntityId,
996
- targetEntityName: r.targetEntityName,
997
- messages: r.messages,
998
- } ) ),
999
1154
  hasIntrospection: !!executiveOutput.introspection,
1000
1155
  hasNarrative: !!executiveOutput.narrative
1001
1156
  } )
@@ -1191,16 +1346,17 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
1191
1346
 
1192
1347
  // Clear processed messages
1193
1348
 
1194
- // Merge escalation percepts into final commands, and retire the ones already
1195
- // honoured. See _dischargedUndertakings for why this is not optional.
1196
- const { keep, discharge } = this._reconcileUndertakings( escalationPercepts, this._lastStateRef! )
1197
- if( keep.length ){
1349
+ // Merge escalation percepts into the final commands.
1350
+ //
1351
+ // `_reconcileUndertakings` used to sit here, discharging promises the engine
1352
+ // judged kept and dropping restatements. Both are gone with the undertaking
1353
+ // harness: a promise is no longer a percept, so there is nothing to
1354
+ // reconcile. What the facet declared rides the sync tract to the master's own
1355
+ // prompt, and what to do about it — including whether it is still worth
1356
+ // doing — is the mind's, through the goal machinery it already has.
1357
+ if( escalationPercepts.length ){
1198
1358
  commands.set ??= []
1199
- commands.set.push( ...keep )
1200
- }
1201
- if( discharge.length ){
1202
- commands.delete ??= []
1203
- commands.delete.push( ...discharge )
1359
+ commands.set.push( ...escalationPercepts )
1204
1360
  }
1205
1361
 
1206
1362
  // ── Persist deliberation cache (mirrors the rolling summary) ──
@@ -1271,7 +1427,13 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
1271
1427
 
1272
1428
  return [ ...this._facetSubjects.values() ]
1273
1429
  .sort( ( a, b ) => b.tick - a.tick )
1274
- .map( s => ({ entityId: s.entityId, ...( s.name ? { name: s.name } : {} ), sinceTick: s.tick }) )
1430
+ .map( s => ({
1431
+ entityId: s.entityId,
1432
+ ...( s.name ? { name: s.name } : {} ),
1433
+ sinceTick: s.tick,
1434
+ ...( s.concluded ? { concluded: s.concluded } : {} ),
1435
+ ...( s.promised?.length ? { promised: s.promised } : {} ),
1436
+ }) )
1275
1437
  }
1276
1438
 
1277
1439
  /**
@@ -1311,76 +1473,6 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
1311
1473
  * claim that the words are unsent; whether to say more to that person is then an
1312
1474
  * ordinary competition like any other.
1313
1475
  */
1314
- private _reconcileUndertakings(
1315
- incoming: EntityInput[],
1316
- state: ReadonlySimulationState,
1317
- ): { keep: EntityInput[]; discharge: string[] } {
1318
- /** Latest tick at which anything was sent to each target. */
1319
- const contacted = new Map<string, number>()
1320
- for( const [ , e ] of state.entities ){
1321
- if( e.type !== 'conversation.sent') continue
1322
- const m = e.metadata as Record<string, unknown> | undefined
1323
- const target = typeof m?.['targetEntityId'] === 'string' ? m['targetEntityId'] as string : undefined
1324
- if( !target ) continue
1325
- // metadata.tick where set, else the entity's own sim-clock tick. Reading
1326
- // metadata alone defaulted every record to 0, so `contacted >= madeAt` was
1327
- // false for any promise made after tick 0 — this discharge never once fired.
1328
- const at = typeof m?.['tick'] === 'number' ? m['tick'] as number
1329
- : typeof ( e as { tick?: number } ).tick === 'number' ? ( e as { tick?: number } ).tick as number
1330
- : 0
1331
- contacted.set( target, Math.max( contacted.get( target ) ?? 0, at ) )
1332
- }
1333
-
1334
- /** An undertaking is spent once we have reached its target since making it. */
1335
- const honoured = ( target: string | undefined, madeAt: number ): boolean =>
1336
- !!target && ( contacted.get( target ) ?? -1 ) >= madeAt
1337
-
1338
- const undertakingOf = ( m: Record<string, unknown> | undefined ) =>
1339
- m?.['category'] === 'undertaking'
1340
- ? {
1341
- target: typeof m['undertakingTarget'] === 'string' ? m['undertakingTarget'] as string : undefined,
1342
- madeAt: typeof m['tick'] === 'number' ? m['tick'] as number : 0,
1343
- }
1344
- : undefined
1345
-
1346
- const discharge: string[] = []
1347
- /** Targets we are already carrying a live, unhonoured undertaking toward. */
1348
- const carrying = new Set<string>()
1349
-
1350
- for( const [ id, e ] of state.entities ){
1351
- if( e.type !== 'percept') continue
1352
- const u = undertakingOf( e.metadata as Record<string, unknown> | undefined )
1353
- if( !u ) continue
1354
-
1355
- // No target ⇒ it predates the field this reconciliation keys on, so it can
1356
- // never be matched against a contact and would assert "nothing has gone to
1357
- // them yet" for the rest of the mind's life. Seven of these were found in a
1358
- // live snapshot, each one re-read every cycle. Undischargeable is strictly
1359
- // worse than gone.
1360
- if( !u.target ){ discharge.push( id ); continue }
1361
-
1362
- if( honoured( u.target, u.madeAt ) ) discharge.push( id )
1363
- else carrying.add( u.target )
1364
- }
1365
-
1366
- // Drop an incoming restatement of something already honoured or already held.
1367
- // Duplicates were how seven of these piled up: each master cycle drained a
1368
- // fresh copy of a promise that could never be marked kept.
1369
- const keep = incoming.filter( p => {
1370
- const u = undertakingOf( p.metadata as Record<string, unknown> | undefined )
1371
- if( !u?.target ) return true
1372
- if( honoured( u.target, u.madeAt ) ) return false
1373
- if( carrying.has( u.target ) ) return false
1374
- carrying.add( u.target )
1375
- return true
1376
- } )
1377
-
1378
- if( discharge.length )
1379
- logger.info(`[executive] ${ discharge.length } undertaking(s) discharged — the contact was made`)
1380
-
1381
- return { keep, discharge }
1382
- }
1383
-
1384
1476
  private _onFacetSync( event: CognitiveEvent ): void {
1385
1477
  const payload = event.payload as {
1386
1478
  facetId?: string
@@ -1391,13 +1483,28 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
1391
1483
  subjectName?: string
1392
1484
  }
1393
1485
 
1394
- // Remember WHO this facet is with, not just that it reported.
1395
- if( payload.facetId && payload.subjectEntityId )
1486
+ // Remember WHO this facet is with, and WHAT it worked out there. The second
1487
+ // half is the return leg: `reasoning` has always been on this payload and was
1488
+ // read into a local and then dropped, so the master learned that its attention
1489
+ // had been engaged and never what it concluded.
1490
+ //
1491
+ // Carried whole, not clipped. For the master this is the only copy — the
1492
+ // facet's own history is in the facet, and once that conversation ends the
1493
+ // thinking is gone. Live facets are few (one per open conversation), so this
1494
+ // is bounded by how many people the mind is talking to, not by traffic.
1495
+ if( payload.facetId && payload.subjectEntityId ){
1496
+ const prior = this._facetSubjects.get( payload.facetId )
1396
1497
  this._facetSubjects.set( payload.facetId, {
1397
1498
  entityId: payload.subjectEntityId,
1398
1499
  ...( payload.subjectName ? { name: payload.subjectName } : {} ),
1399
1500
  tick: payload.tick ?? this._lastExecutiveTick ?? 0,
1501
+ ...( payload.reasoning ? { concluded: payload.reasoning } : {} ),
1502
+ // Promises ride along rather than being re-derived: a commitment the facet
1503
+ // DECLARED is a fact, while the same thing read back out of prose is a
1504
+ // guess. Preserved across syncs until the master has read them.
1505
+ ...( prior?.promised?.length ? { promised: prior.promised } : {} ),
1400
1506
  })
1507
+ }
1401
1508
 
1402
1509
  // Unconditional (not gated on WORKSPACE_THRESHOLD like ordinary traffic):
1403
1510
  // a facet reporting back is the master's own attention returning, and the
@@ -1461,31 +1568,81 @@ export class ExecutiveEngine extends AsyncEngine implements CognitiveEngine {
1461
1568
  // unfalsifiable: it could be marked kept by something that happened first.
1462
1569
  const tick = payload.tick ?? this._currentTick
1463
1570
 
1571
+ // AN UNDERTAKING RIDES THE TRACT, NOT A HARNESS OF ITS OWN.
1572
+ //
1573
+ // It used to become a percept plus a standing ideomotor intent plus a bespoke
1574
+ // discharge rule — the engine deciding how hard a promise pulls and when it
1575
+ // counts as kept. That is judgement, and judgement is the mind's. What is not
1576
+ // the mind's to supply is the FACT crossing from where its attention was to
1577
+ // where its singular seat is; that crossing is anatomy.
1578
+ //
1579
+ // So it is filed with what the facet concluded and rendered to the master at
1580
+ // its next cycle. Reading that it promised someone something and has not done
1581
+ // it, the master may form a goal — which it can already do, and which the goal
1582
+ // machinery then lifts, plans and completes. Or it may not, and that is a
1583
+ // decision rather than a mechanism failing.
1584
+ //
1585
+ // The old route could not have worked anyway: a percept is swept at +2 ticks
1586
+ // and decays out of working memory at about +9, while the master's interval
1587
+ // is 15. The notice was usually gone before the seat it was addressed to ran.
1588
+ // Spike the salience buffer FIRST, for both kinds. The master must NOT be
1589
+ // handed the inbound as a message to answer — that would make it produce a
1590
+ // [REPLY], duplicating the facet's and breaking the facet/master boundary.
1591
+ // (The queue that once carried inbound this way was removed in #114: nothing
1592
+ // ever filled it.) Done before the undertaking branch returns, so a promise
1593
+ // still pulls the master's next cycle forward rather than waiting out the
1594
+ // interval — which matters more now that the promise is what it will read.
1595
+ this._gatingState.salienceBuffer.push({ event, tick })
1596
+
1597
+ const from = payload.subjectName ?? payload.subjectEntityId ?? payload.facetId ?? 'a focus'
1598
+
1599
+ if( payload.body.kind === 'undertaking'){
1600
+ const body = payload.body
1601
+ // Keyed by facet because that is what the master's view of its open threads
1602
+ // is keyed by. A handoff with no facet id has nowhere to file — say so
1603
+ // rather than dropping it quietly, which is how a promise disappears with
1604
+ // nobody able to tell whether it was ever made.
1605
+ if( !payload.facetId ){
1606
+ logger.warn(`[executive] commitment "${ body.what }" arrived with no facetId — nowhere to file it`)
1607
+ return
1608
+ }
1609
+ // Created if absent: a facet can hand something up before its first sync,
1610
+ // and requiring the entry to exist first made that promise vanish.
1611
+ const at = this._facetSubjects.get( payload.facetId ) ?? {
1612
+ entityId: payload.subjectEntityId ?? payload.facetId,
1613
+ ...( payload.subjectName ? { name: payload.subjectName } : {} ),
1614
+ tick,
1615
+ }
1616
+ at.promised = [
1617
+ // Deduped on WHAT was promised, falling back to the target when two
1618
+ // wordings mean the same contact: restating a promise is the same
1619
+ // promise, and the master does not need telling twice.
1620
+ ...( at.promised ?? [] ).filter( p =>
1621
+ p.what !== body.what && !( body.target !== undefined && p.target === body.target ) ),
1622
+ { what: body.what,
1623
+ ...( body.target ? { target: body.target } : {} ),
1624
+ ...( body.gist ? { gist: body.gist } : {} ), tick },
1625
+ ]
1626
+ this._facetSubjects.set( payload.facetId, at )
1627
+ logger.info(
1628
+ `[executive] filed commitment from ${from} → "${ body.what }" ` +
1629
+ `(it reads this at its next cycle; what to do about it is its own call)`
1630
+ )
1631
+ return
1632
+ }
1633
+
1464
1634
  this._escalations.push({
1465
1635
  tick,
1466
1636
  ...( payload.facetId ? { facetId: payload.facetId } : {} ),
1467
1637
  ...( payload.subjectEntityId ? { subjectEntityId: payload.subjectEntityId } : {} ),
1468
1638
  ...( payload.subjectName ? { subjectName: payload.subjectName } : {} ),
1469
1639
  ...( payload.threadId ? { threadId: payload.threadId } : {} ),
1470
- body: payload.body.kind === 'undertaking'
1471
- ? { ...payload.body, reasoning: ( payload.body.reasoning ?? '').slice( 0, 400 ) }
1472
- : { ...payload.body, reasoning: ( payload.body.reasoning ?? '').slice( 0, 400 ) },
1640
+ body: { ...payload.body, reasoning: ( payload.body.reasoning ?? '').slice( 0, 400 ) },
1473
1641
  })
1474
1642
 
1475
- // Spike the salience buffer so the master fires soon rather than waiting for
1476
- // the next scheduled interval. The master must NOT be handed the inbound as a
1477
- // message to answer — that would make it produce a [REPLY], duplicating the
1478
- // facet's and breaking the facet/master boundary. (The queue that once carried
1479
- // inbound this way was removed in #114: nothing ever filled it.)
1480
- this._gatingState.salienceBuffer.push({ event, tick })
1481
-
1482
- const from = payload.subjectName ?? payload.subjectEntityId ?? payload.facetId ?? 'a focus'
1483
1643
  logger.info(
1484
- payload.body.kind === 'undertaking'
1485
- ? `[executive] master queued undertaking from ${from} ` +
1486
- `→ reach ${payload.body.target} (confidence=${payload.confidence?.toFixed( 2 )})`
1487
- : `[executive] master queued escalation percept from ${from} ` +
1488
- `(confidence=${payload.confidence?.toFixed( 2 )})`
1644
+ `[executive] master queued escalation percept from ${from} ` +
1645
+ `(confidence=${payload.confidence?.toFixed( 2 )})`
1489
1646
  )
1490
1647
  }
1491
1648