@mindot/will 0.7.0 → 0.9.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 (88) hide show
  1. package/README.md +87 -22
  2. package/dist/channels/discord.d.ts +67 -6
  3. package/dist/channels/discord.js +112 -6
  4. package/dist/channels/discord.js.map +1 -1
  5. package/dist/channels/whatsapp.d.ts +1 -1
  6. package/dist/channels/whatsapp.js +4 -1
  7. package/dist/channels/whatsapp.js.map +1 -1
  8. package/dist/cli.js +13733 -11057
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +3729 -1199
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/effectors.d.ts +1 -1
  14. package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
  15. package/package.json +1 -1
  16. package/src/channels/discord.ts +189 -11
  17. package/src/channels/types.ts +90 -0
  18. package/src/channels/whatsapp.ts +13 -4
  19. package/src/cli.ts +9 -4
  20. package/src/cognition/agency/consequence.ts +122 -1
  21. package/src/cognition/agency/conversation.aim.ts +260 -0
  22. package/src/cognition/agency/engines/action.selector.ts +85 -3
  23. package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
  24. package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
  25. package/src/cognition/agency/engines/reafference.engine.ts +129 -2
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +16 -2
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/repertoire.ts +12 -5
  30. package/src/cognition/agency/selection.scoring.ts +33 -0
  31. package/src/cognition/agency/types.ts +35 -0
  32. package/src/cognition/cache/composition.ts +232 -0
  33. package/src/cognition/cache/deliberation.cache.ts +219 -0
  34. package/src/cognition/cache/fingerprint.ts +120 -0
  35. package/src/cognition/cache/types.ts +105 -0
  36. package/src/cognition/config.mirror.entities.ts +109 -1
  37. package/src/cognition/event.schemas.ts +22 -0
  38. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  39. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  40. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  41. package/src/cognition/faculties/executive.engine/context.ts +67 -13
  42. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  43. package/src/cognition/faculties/executive.engine/engine.ts +686 -187
  44. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
  45. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
  46. package/src/cognition/faculties/executive.engine/facet.ts +89 -26
  47. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  48. package/src/cognition/faculties/executive.engine/parser.ts +21 -1
  49. package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
  50. package/src/cognition/faculties/executive.engine/types.ts +69 -0
  51. package/src/cognition/faculties/goal.manager.ts +94 -14
  52. package/src/cognition/faculties/known.entity.tracker.ts +267 -28
  53. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  54. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  55. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  56. package/src/cognition/faculties/self.model.updater.ts +19 -12
  57. package/src/cognition/faculties/social.perception.ts +47 -3
  58. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  59. package/src/cognition/faculties/working.memory.ts +10 -20
  60. package/src/cognition/identity.entity.ts +205 -0
  61. package/src/cognition/index.ts +11 -0
  62. package/src/cognition/memory/vector.adapter.ts +12 -3
  63. package/src/cognition/memory/vector.embedder.ts +54 -7
  64. package/src/cognition/persona.prior.ts +6 -0
  65. package/src/cognition/senses/audition.engine/engine.ts +404 -46
  66. package/src/cognition/senses/base.sense.engine.ts +1 -1
  67. package/src/cognition/senses/index.ts +12 -0
  68. package/src/cognition/social.identity.ts +273 -0
  69. package/src/cognition/utilities/token.tracker.ts +246 -98
  70. package/src/core/orchestrator.ts +38 -0
  71. package/src/host/boot.ts +78 -22
  72. package/src/index.ts +35 -0
  73. package/src/llm/index.ts +415 -97
  74. package/src/llm/routing.ts +204 -0
  75. package/src/llm/summarizer.ts +5 -1
  76. package/src/llm/wire.contracts.ts +19 -0
  77. package/src/pma/index.ts +67 -53
  78. package/src/runners/thin-shim.runner.ts +18 -6
  79. package/src/sdk/will.ts +121 -22
  80. package/src/stem/assembly.audit.ts +1 -0
  81. package/src/stem/guards/identity.coherence.ts +17 -6
  82. package/src/stem/index.ts +82 -5
  83. package/src/stem/mind.ts +327 -79
  84. package/src/stem/policy/arbiter.ts +49 -14
  85. package/src/stem/policy/rule.table.ts +2 -2
  86. package/src/stem/tracts/effector.controller.ts +56 -9
  87. package/src/stem/tracts/outbox.writer.ts +40 -2
  88. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
@@ -15,8 +15,18 @@
15
15
  // caps how many may run at once:
16
16
  // • _facets — live facet instances, keyed by id
17
17
  // • _facetCounter — monotonic id source (facet-1, facet-2, …)
18
+ // • _byKey/_keyOf — a facet's THREAD identity (`FacetSpawnDeps.key`), so one
19
+ // subject is one facet however many callers ask for it, and so eviction can
20
+ // tell an open conversation from a one-shot transient.
21
+ // • _reasoningByKey — a thread's own reasoning, outliving the instance that
22
+ // carried it, so re-opening resumes rather than starting cold.
18
23
  // • _attentionFreeCapacity — free attention (0–1), updated from
19
- // `attention.state.changed`; one facet is allowed per ~0.3 free units.
24
+ // `attention.state.changed`; it SCALES the ceiling below rather than being it.
25
+ // • the ceiling itself is `engine-config-executive.maxFacets` read through the
26
+ // persona-prior — how many things this particular mind can hold at once, a
27
+ // trait the metacognition loop develops, not a constant. Its sibling
28
+ // `facetIdleTtlTicks` — how long a QUIET thread stays open — is read the same
29
+ // way, for the same reason.
20
30
  //
21
31
  // It also keeps `_lastStateRef` / `_sessionLogger` fresh so a facet's
22
32
  // deferred destroy() logs against the same live values the engine would —
@@ -24,9 +34,11 @@
24
34
  // engine's own state ref changes), so the two never diverge.
25
35
  //
26
36
  // Deliberately left in the engine (not here):
27
- // • the `executive.facet.sync` / `audition.task.signal` bus subscriptions
28
- // and the `_facetSyncSubscribed` guard they push into the engine's
29
- // gating salience buffer and are shared with the escalation path;
37
+ // • `executive.facet.sync` / `executive.facet.handoff` handling — dispatched from
38
+ // ExecutiveEngine.onCognitiveEvent (the bus keeps ONE subscription per
39
+ // engineId, so a dedicated `subscribe(this.name, …)` is overwritten by the
40
+ // orchestrator's registration); they push into the engine's gating salience
41
+ // buffer and are shared with the escalation path;
30
42
  // • the `executive.master.sync` publish — it reads the master's reasoning
31
43
  // output; the engine publishes it gated on `size`.
32
44
  //
@@ -43,6 +55,34 @@ import { ExecutiveFacet, type ExecutiveFacetHandle } from '#faculties/executive.
43
55
  import type { CompletionInbox } from '#cognition/completion.inbox'
44
56
  import type { ContextDependencies } from '#faculties/executive.engine/context'
45
57
  import type { PromptDependencies } from '#faculties/executive.engine/prompt.factory'
58
+ import { readEffectiveParams } from '#cognition/persona.prior'
59
+
60
+ /**
61
+ * Facets a Will can hold at once when nothing is seeded — the same value
62
+ * `buildEngineConfigEntities` seeds, so a supervisor running against a state with
63
+ * no engine-config mirror (unit tests, bare harnesses) behaves like a real mind
64
+ * rather than collapsing to the old cap of 3.
65
+ */
66
+ const DEFAULT_MAX_FACETS = 10
67
+
68
+ /**
69
+ * Ticks of quiet before a facet is reaped, when nothing is seeded.
70
+ *
71
+ * This was a hardcoded 50 and it was catastrophic. Measured on a live Will at
72
+ * 1.64 ticks/second, 50 ticks is THIRTY SECONDS — so a conversation facet was
73
+ * destroyed half a minute after the human stopped typing. Her operator's replies
74
+ * were one to three minutes apart, which means every message he sent arrived at a
75
+ * brand-new facet with an empty reasoning history: 290 spawns and 235 destroys in
76
+ * a 29-minute session, ten a minute. She was not losing the thread; the thread was
77
+ * being destroyed between his turns, and re-asking the question he had already
78
+ * answered is exactly what a cold facet does.
79
+ *
80
+ * The right scale is human, not machine: a conversation stays open across the pause
81
+ * where someone goes to make coffee. 3000 ticks is ~30 minutes at that rate. Nothing
82
+ * is leaked by being generous here — `maxFacets` plus LRU eviction is what actually
83
+ * bounds the population, and this only decides when a QUIET thread is considered over.
84
+ */
85
+ const DEFAULT_FACET_IDLE_TTL_TICKS = 3000
46
86
 
47
87
  /**
48
88
  * The engine attachments a spawn needs, captured at call time. bus / director
@@ -58,10 +98,41 @@ export interface FacetSpawnDeps {
58
98
  willId: string | null
59
99
  /** Tick-boundary landing for decision effects (see cognition/completion.inbox). */
60
100
  inbox?: CompletionInbox | null
101
+ /**
102
+ * What this facet is FOR — a caller-chosen identity for the thread, e.g.
103
+ * `conversation:discord:1019…`. Two spawns with the same key are the same
104
+ * thread of attention, so the second returns the first's handle instead of
105
+ * opening a rival facet on the same subject.
106
+ *
107
+ * Keying is opt-in because not every facet has a subject (a one-shot
108
+ * deliberation does not). But a keyless facet is also the one the mind can
109
+ * most afford to lose, so eviction takes those first — see _evictionVictim.
110
+ *
111
+ * Without this the registry was keyed only by `facet-N` and every caller
112
+ * deduplicated (or didn't) on its own: AuditionEngine kept one facet per
113
+ * speaker, `authorOutreach` kept none, and a proactive message to someone
114
+ * could evict the live conversation with that same person.
115
+ */
116
+ key?: string
61
117
  }
62
118
 
63
- export type SpawnResult =
64
- { attention: 'available' | 'full', handle?: ExecutiveFacetHandle }
119
+ export type SpawnResult = { attention: 'available' | 'full', handle?: ExecutiveFacetHandle }
120
+
121
+ /**
122
+ * How a facet gets built. Exists so a caller can stand something else in place of
123
+ * a real reasoning loop — chiefly a test that wants to observe the REGISTRY
124
+ * (keying, eviction, continuity) without paying for an LLM director.
125
+ *
126
+ * A seam rather than a module mock, deliberately. `mock.module` / `vi.mock` is
127
+ * process-global and permanent in Bun's runner: one file mocking this module
128
+ * replaced ExecutiveFacet for every file that loaded AFTER it, in the same
129
+ * process. That turned a green branch red — the audition reply tests sat waiting
130
+ * on a facet whose `pump()` was a no-op and timed out at 30s — and because the
131
+ * damage follows file order, the failing set differed between CI and a local run,
132
+ * which reads exactly like flake. Injection is scoped to the supervisor that asked
133
+ * for it and cannot reach anybody else.
134
+ */
135
+ export type FacetFactory = ( facetId: string, deps: FacetSpawnDeps ) => ExecutiveFacet
65
136
 
66
137
  export class FacetSupervisor {
67
138
  private _facets = new Map<string, ExecutiveFacet>()
@@ -75,19 +146,110 @@ export class FacetSupervisor {
75
146
 
76
147
  /** Reap callbacks per facet — fired when the supervisor reaps (idle/LRU), not on explicit destroy(). */
77
148
  private _onReaped = new Map<string, () => void>()
78
- /** Ticks of inactivity before a facet is reaped (reclaims its attention budget). */
79
- private readonly _idleTtlTicks: number
149
+
150
+ /** Live handles by facetId — returned again when a keyed spawn matches an open thread. */
151
+ private _handles = new Map<string, ExecutiveFacetHandle>()
152
+ /** `FacetSpawnDeps.key` → facetId, for the keyed spawns. */
153
+ private _byKey = new Map<string, string>()
154
+ /** facetId → its key, so reaping can clear both directions. */
155
+ private _keyOf = new Map<string, string>()
156
+
157
+ /**
158
+ * A keyed thread's own prior reasoning, surviving the facet that produced it.
159
+ *
160
+ * A facet reaped mid-conversation used to take its train of thought with it: the
161
+ * transcript survived (AuditionEngine holds the digest by thread) but everything
162
+ * the mind had privately worked out about that person was gone, so the replacement
163
+ * facet re-derived it — or, more often, re-asked. Continuity belongs to the thread,
164
+ * not to the instance that happened to be carrying it.
165
+ */
166
+ private _reasoningByKey = new Map<string, string[]>()
167
+
168
+ /** Constructor override for the idle TTL — null means read it from the persona. */
169
+ private readonly _idleTtlOverride: number | null
80
170
  /** When the budget is full, evict the least-recently-active facet instead of refusing a spawn. */
81
171
  private readonly _evictLruOnPressure: boolean
172
+ /** What a spawn constructs — see FacetFactory. */
173
+ private readonly _createFacet: FacetFactory
82
174
 
83
- constructor( opts: { idleTtlTicks?: number; evictLruOnPressure?: boolean } = {} ){
84
- this._idleTtlTicks = opts.idleTtlTicks ?? 50
175
+ constructor( opts: { idleTtlTicks?: number; evictLruOnPressure?: boolean; createFacet?: FacetFactory } = {} ){
176
+ this._idleTtlOverride = opts.idleTtlTicks ?? null
85
177
  this._evictLruOnPressure = opts.evictLruOnPressure ?? true
178
+ // `spawn` throws on a missing bus / director / stateRef before it ever gets
179
+ // here, so the factory is only ever called with those present — the same
180
+ // reason `willId` was already asserted at this call.
181
+ this._createFacet = opts.createFacet ?? ( ( facetId, deps ) => new ExecutiveFacet(
182
+ facetId,
183
+ deps.bus!,
184
+ deps.llmDirector!,
185
+ deps.contextDeps,
186
+ deps.promptDeps,
187
+ deps.willId!,
188
+ deps.inbox ?? null
189
+ ) )
190
+ }
191
+
192
+ /**
193
+ * How long a quiet facet lives, in ticks.
194
+ *
195
+ * Read through the persona-prior like `maxFacets`, because it is the same kind of
196
+ * fact about a person: how long a conversation stays open for them before it feels
197
+ * finished. It was the one number in this economy that no personality could move —
198
+ * the ceiling was developable while the thing doing the killing was a constant.
199
+ */
200
+ private _idleTtl( state: ReadonlySimulationState | null ): number {
201
+ if( this._idleTtlOverride != null ) return this._idleTtlOverride
202
+ if( !state ) return DEFAULT_FACET_IDLE_TTL_TICKS
203
+ return Math.max(
204
+ 1,
205
+ Math.round(
206
+ readEffectiveParams( state, 'engine-config-executive').facetIdleTtlTicks
207
+ ?? DEFAULT_FACET_IDLE_TTL_TICKS
208
+ )
209
+ )
86
210
  }
87
211
 
88
212
  /** Number of live facets — the engine gates `master.sync` on this. */
89
213
  get size(): number { return this._facets.size }
90
214
 
215
+ /**
216
+ * The facet already carrying `key`, if any — WITHOUT opening one.
217
+ *
218
+ * Lets a caller ask "am I already attending to this?" and act differently when
219
+ * the answer is yes. The case it exists for: the mind decides, on its own
220
+ * initiative, to say something to someone it is ALREADY in conversation with.
221
+ * That is not a second thread; it is a thing to say in the thread that is open.
222
+ */
223
+ handleFor( key: string ): ExecutiveFacetHandle | undefined {
224
+ const id = this._byKey.get( key )
225
+ return id ? this._handles.get( id ) : undefined
226
+ }
227
+
228
+ /** Ids of the facets currently alive — the engine prunes its subject map against these. */
229
+ liveFacetIds(): Set<string> { return new Set( this._facets.keys() ) }
230
+
231
+ /**
232
+ * Ids of the facets currently REASONING — queued reports or an in-flight
233
+ * `_reason()`. These are what the mind is actually attending to, and the engine
234
+ * turns them into `attention.demand` entities so they cost the AttentionAllocator
235
+ * real capacity.
236
+ *
237
+ * The distinction is the whole two-level model: an open facet is a thread the
238
+ * mind is IN (bounded by the persona's `maxFacets`), a busy one is a thread it is
239
+ * ATTENDING TO (bounded by the allocator's `maxFoci`, and paid for out of the
240
+ * same 100-unit budget as every other focus). You can be in ten conversations and
241
+ * attending to two. Without this, holding conversations cost the allocator
242
+ * nothing, so `freeFraction` — the very signal the facet budget scales on —
243
+ * reported the same spare attention whether the mind was idle or mid-thread with
244
+ * three people.
245
+ */
246
+ busyFacetIds(): string[] {
247
+ const out: string[] = []
248
+ for( const [ id, facet ] of this._facets )
249
+ if( facet.busy ) out.push( id )
250
+ return out
251
+ }
252
+
91
253
  attachSessionLogger( logger: SessionLogger | null ): void {
92
254
  this._sessionLogger = logger
93
255
  }
@@ -96,10 +258,10 @@ export class FacetSupervisor {
96
258
  * Update the attention budget from an `attention.state.changed` event.
97
259
  *
98
260
  * `freeFraction` is the allocator's normalized 0–1 spare-attention signal
99
- * (free capacity ÷ baseline capacity). It is consumed directly — one facet per
100
- * ~0.3 free units so the budget binds on the same scale the `0.3` constant
101
- * and the default (`1` 3 facets) assume. (Pre-fix this received the raw 0–100
102
- * capacity, inflating the budget ~100× so facets were bounded only by TTL/LRU.)
261
+ * (free capacity ÷ baseline capacity). It scales the persona's facet ceiling:
262
+ * fully free the whole ceiling, half free about half of it, never below 1.
263
+ * (It must stay normalized an earlier version received the raw 0–100 capacity,
264
+ * inflating the budget ~100× so facets were bounded only by TTL/LRU.)
103
265
  */
104
266
  setAttentionState( freeFraction: number ): void {
105
267
  this._attentionFreeCapacity = Math.max( 0, freeFraction )
@@ -133,13 +295,14 @@ export class FacetSupervisor {
133
295
  }
134
296
 
135
297
  private _reapIdle( tick: number ): void {
298
+ const ttl = this._idleTtl( this._lastStateRef )
136
299
  for( const [ id, facet ] of [ ...this._facets ] ){
137
300
  // Never reap a busy facet: queued reports / an in-flight LLM call span
138
301
  // many ticks (a real call is 10–30s), and destroying the facet clears the
139
302
  // listeners its pending decision lands on — a conversation reply would
140
303
  // vanish silently. The TTL measures *quiet* facets only.
141
304
  if( facet.busy ) continue
142
- if( tick - facet.lastActiveTick > this._idleTtlTicks )
305
+ if( tick - facet.lastActiveTick > ttl )
143
306
  this._reap( id, 'idle')
144
307
  }
145
308
  }
@@ -149,8 +312,19 @@ export class FacetSupervisor {
149
312
  const facet = this._facets.get( facetId )
150
313
  if( !facet ) return
151
314
 
315
+ // Keep the thread's reasoning before the instance carrying it is destroyed,
316
+ // so a later spawn on the same key resumes rather than starting cold.
317
+ const key = this._keyOf.get( facetId )
318
+ if( key ){
319
+ const carried = facet.reasoningHistory
320
+ if( carried.length ) this._reasoningByKey.set( key, carried )
321
+ this._byKey.delete( key )
322
+ this._keyOf.delete( facetId )
323
+ }
324
+
152
325
  facet.destroy()
153
326
  this._facets.delete( facetId )
327
+ this._handles.delete( facetId )
154
328
  const onReaped = this._onReaped.get( facetId )
155
329
  this._onReaped.delete( facetId )
156
330
 
@@ -168,20 +342,33 @@ export class FacetSupervisor {
168
342
  catch( err ){ logger.error(`[executive] facet ${facetId} onReaped error:`, err ) }
169
343
  }
170
344
 
171
- private _leastRecentlyActive(): string | null {
172
- // Prefer a quiet victim: evicting a busy facet drops its in-flight decision
173
- // (same silent-loss mode the idle reaper guards against). Only when every
174
- // facet is busy does pressure eviction fall back to the absolute LRU — a
175
- // new conversation still preempts rather than being refused.
176
- let id: string | null = null
177
- let min = Infinity
178
- for( const [ fid, facet ] of this._facets )
179
- if( !facet.busy && facet.lastActiveTick < min ){ min = facet.lastActiveTick; id = fid }
180
- if( id ) return id
181
-
182
- for( const [ fid, facet ] of this._facets )
183
- if( facet.lastActiveTick < min ){ min = facet.lastActiveTick; id = fid }
184
- return id
345
+ /**
346
+ * Who gets evicted when the budget is full, in order of what the mind can
347
+ * afford to lose:
348
+ *
349
+ * 1. quiet AND keyless a transient facet (a one-shot authoring pass, a
350
+ * deliberation) with nobody on the other end of it;
351
+ * 2. quiet but keyed — an open thread with a real subject;
352
+ * 3. busy last resort, and it drops an in-flight decision.
353
+ *
354
+ * Tier 1 exists because of an observed inversion: a proactive outreach spawn
355
+ * would evict the LIVE CONVERSATION FACET WITH THAT SAME PERSON. A conversation
356
+ * facet waiting on the human's next message is, correctly, not `busy` so it was
357
+ * the most attractive LRU victim in the registry, and deciding to message someone
358
+ * destroyed the conversation already open with them.
359
+ */
360
+ private _evictionVictim(): string | null {
361
+ const oldestIn = ( pick: ( id: string, f: ExecutiveFacet ) => boolean ): string | null => {
362
+ let id: string | null = null
363
+ let min = Infinity
364
+ for( const [ fid, facet ] of this._facets )
365
+ if( pick( fid, facet ) && facet.lastActiveTick < min ){ min = facet.lastActiveTick; id = fid }
366
+ return id
367
+ }
368
+
369
+ return oldestIn( ( fid, f ) => !f.busy && !this._keyOf.has( fid ) )
370
+ ?? oldestIn( ( _fid, f ) => !f.busy )
371
+ ?? oldestIn( () => true )
185
372
  }
186
373
 
187
374
  /**
@@ -210,33 +397,67 @@ export class FacetSupervisor {
210
397
  // destroy() logs the same tick the engine would.
211
398
  this._lastStateRef = deps.stateRef
212
399
 
213
- // One facet per ~0.3 free capacity units, floor at 1
214
- const maxFacets = Math.max( 1, Math.floor( this._attentionFreeCapacity / 0.3 ) )
400
+ // Same key same thread of attention. Hand back the facet already carrying
401
+ // it rather than opening a rival on the same subject: two facets on one person
402
+ // is two minds answering as one, and both of them were reported to `_facetSubjects`,
403
+ // so the master read itself as being in two conversations with the same person.
404
+ if( deps.key ){
405
+ const openId = this._byKey.get( deps.key )
406
+ const open = openId ? this._handles.get( openId ) : undefined
407
+ if( open ){
408
+ this._facets.get( openId! )?.markActive( deps.stateRef.tick as unknown as number )
409
+ logger.info(`[executive] facet ${openId} reused for "${deps.key}"`)
410
+ return { attention: 'available', handle: open }
411
+ }
412
+ }
413
+
414
+ // How many focused facets this mind can hold at once.
415
+ //
416
+ // This is the OPEN-THREAD level. Its sibling is the allocator's
417
+ // `engine-config-attention.maxFoci` — how many things the mind ATTENDS TO at
418
+ // once — and the two are one economy, not two budgets: a facet that is actually
419
+ // reasoning is published as an `attention` (ExecutiveEngine.
420
+ // _facetAttentionDemands) and competes for those foci slots against every
421
+ // percept, paying `costPerFocus` out of the same 100-unit capacity. So a Will
422
+ // can be IN ten conversations while ATTENDING TO two, and the cost of the ones
423
+ // it is attending to flows back into the free fraction below. The same
424
+ // openness/conscientiousness pair develops both levels (consolidator 27c/27d),
425
+ // so they move together rather than drifting apart.
426
+ //
427
+ // Two layers, deliberately separate:
428
+ // • the CEILING is who this person is — `engine-config-executive.maxFacets`
429
+ // read through the persona-prior, so openness widens it and
430
+ // conscientiousness narrows it as the Will demonstrates those traits. It is
431
+ // a property of the mind, not a constant in the code.
432
+ // • the live ALLOWANCE is how loaded it is right now — spare attention scales
433
+ // the ceiling, so a tired or saturated mind takes on fewer new threads and
434
+ // recovers the room as attention frees up.
435
+ //
436
+ // Previously the second layer WAS the ceiling (one facet per 0.3 free units,
437
+ // max 3), which put a hard architectural cap on the mind that no personality
438
+ // could move: at 52% night capacity it resolved to exactly 1, so a Will could
439
+ // hold one conversation and every second person to speak evicted the first.
440
+ const ceiling = Math.max( 1, Math.round( readEffectiveParams( deps.stateRef, 'engine-config-executive').maxFacets ?? DEFAULT_MAX_FACETS ) )
441
+ const maxFacets = Math.max( 1, Math.min( ceiling, Math.round( ceiling * this._attentionFreeCapacity ) ) )
442
+
215
443
  if( this._facets.size >= maxFacets ){
216
444
  if( !this._evictLruOnPressure ){
217
445
  logger.info(`[executive] attention full (${this._facets.size}/${maxFacets} facets) `)
218
446
  return { attention: 'full' }
219
447
  }
220
- // Budget full but a new conversation arrived — evict the least-recently-active
221
- // facet so a fresh, live conversation preempts a stale one (no silent drop).
222
- const lru = this._leastRecentlyActive()
448
+ // Budget full but a new thread arrived — evict the cheapest loss (transient
449
+ // and quiet first, an open conversation only if there is nothing else) so a
450
+ // live thread preempts a stale one without a silent drop.
451
+ const lru = this._evictionVictim()
223
452
  if( !lru ) return { attention: 'full' } // nothing evictable (shouldn't happen)
224
- logger.info(`[executive] attention full (${this._facets.size}/${maxFacets}) — evicting LRU facet ${lru}`)
453
+ logger.info(`[executive] attention full (${this._facets.size}/${maxFacets}) — evicting facet ${lru}`)
225
454
  this._reap( lru, 'lru')
226
455
  }
227
456
 
228
457
  this._facetCounter++
229
458
  const facetId = `facet-${this._facetCounter}`
230
459
 
231
- const facet = new ExecutiveFacet(
232
- facetId,
233
- deps.bus,
234
- deps.llmDirector,
235
- deps.contextDeps,
236
- deps.promptDeps,
237
- deps.willId!,
238
- deps.inbox ?? null
239
- )
460
+ const facet = this._createFacet( facetId, deps )
240
461
 
241
462
  // Attach session logger if available
242
463
  if( this._sessionLogger )
@@ -248,38 +469,62 @@ export class FacetSupervisor {
248
469
 
249
470
  this._facets.set( facetId, facet )
250
471
 
472
+ if( deps.key ){
473
+ this._byKey.set( deps.key, facetId )
474
+ this._keyOf.set( facetId, deps.key )
475
+
476
+ // Resume the thread's own thinking. The words were never lost (the digest
477
+ // survives with the thread); what was lost was everything the mind had
478
+ // privately worked out about this person, which is why a replacement facet
479
+ // re-asked what it had already been told.
480
+ const carried = this._reasoningByKey.get( deps.key )
481
+ if( carried?.length ){
482
+ facet.restoreReasoningHistory( carried )
483
+ logger.info(`[executive] facet ${facetId} resumed "${deps.key}" (${carried.length} prior turns)`)
484
+ }
485
+ }
486
+
251
487
  logger.info(`[executive] spawned facet → ${facetId} (total facets: ${this._facets.size})`)
252
488
 
253
489
  this._sessionLogger?.write({
254
490
  type: 'executive.facet.spawn',
255
491
  tick: deps.stateRef.tick as unknown as number,
256
492
  facetId,
493
+ ...( deps.key ? { key: deps.key } : {} ),
257
494
  totalFacets: this._facets.size,
258
495
  } as any)
259
496
 
260
- return {
261
- attention: 'available',
262
- handle: {
263
- facetId,
264
- setFocus: ( focus ) => facet.setFocus( focus ),
265
- setStateRef: ( state ) => facet.setStateRef( state ),
266
- report: ( report ) => facet.report( report ),
267
- subscribe: ( listener ) => facet.subscribe( listener ),
268
- onChunk: ( handler ) => facet.setChunkHandler( handler ),
269
- onReaped: ( handler ) => { this._onReaped.set( facetId, handler ) },
270
- destroy: () => {
271
- facet.destroy()
272
- this._facets.delete( facetId )
273
- this._onReaped.delete( facetId ) // explicit close — owner already knows; don't fire onReaped
274
- logger.info(`[executive] facet ${facetId} destroyed (remaining: ${this._facets.size})`)
275
- this._sessionLogger?.write({
276
- type: 'executive.facet.destroy',
277
- tick: this._lastStateRef?.tick as unknown as number ?? 0,
278
- facetId,
279
- totalFacets: this._facets.size,
280
- } as any)
497
+ const handle: ExecutiveFacetHandle = {
498
+ facetId,
499
+ setFocus: ( focus ) => facet.setFocus( focus ),
500
+ setStateRef: ( state ) => facet.setStateRef( state ),
501
+ report: ( report ) => facet.report( report ),
502
+ subscribe: ( listener ) => facet.subscribe( listener ),
503
+ onChunk: ( handler ) => facet.setChunkHandler( handler ),
504
+ onReaped: ( handler ) => { this._onReaped.set( facetId, handler ) },
505
+ destroy: () => {
506
+ const key = this._keyOf.get( facetId )
507
+ if( key ){
508
+ const carried = facet.reasoningHistory
509
+ if( carried.length ) this._reasoningByKey.set( key, carried )
510
+ this._byKey.delete( key )
511
+ this._keyOf.delete( facetId )
281
512
  }
513
+ facet.destroy()
514
+ this._facets.delete( facetId )
515
+ this._handles.delete( facetId )
516
+ this._onReaped.delete( facetId ) // explicit close — owner already knows; don't fire onReaped
517
+ logger.info(`[executive] facet ${facetId} destroyed (remaining: ${this._facets.size})`)
518
+ this._sessionLogger?.write({
519
+ type: 'executive.facet.destroy',
520
+ tick: this._lastStateRef?.tick as unknown as number ?? 0,
521
+ facetId,
522
+ totalFacets: this._facets.size,
523
+ } as any)
282
524
  }
283
525
  }
526
+
527
+ this._handles.set( facetId, handle )
528
+ return { attention: 'available', handle }
284
529
  }
285
530
  }