@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.
- package/README.md +87 -22
- package/dist/channels/discord.d.ts +67 -6
- package/dist/channels/discord.js +112 -6
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/whatsapp.d.ts +1 -1
- package/dist/channels/whatsapp.js +4 -1
- package/dist/channels/whatsapp.js.map +1 -1
- package/dist/cli.js +13733 -11057
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3729 -1199
- package/dist/index.js.map +1 -1
- package/dist/mcp/effectors.d.ts +1 -1
- package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
- package/package.json +1 -1
- package/src/channels/discord.ts +189 -11
- package/src/channels/types.ts +90 -0
- package/src/channels/whatsapp.ts +13 -4
- package/src/cli.ts +9 -4
- package/src/cognition/agency/consequence.ts +122 -1
- package/src/cognition/agency/conversation.aim.ts +260 -0
- package/src/cognition/agency/engines/action.selector.ts +85 -3
- package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
- package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
- package/src/cognition/agency/engines/reafference.engine.ts +129 -2
- package/src/cognition/agency/proactive.communicator.ts +19 -3
- package/src/cognition/agency/reconcile.learning.ts +16 -2
- package/src/cognition/agency/restart.ts +66 -0
- package/src/cognition/agency/schemas/repertoire.ts +12 -5
- package/src/cognition/agency/selection.scoring.ts +33 -0
- package/src/cognition/agency/types.ts +35 -0
- package/src/cognition/cache/composition.ts +232 -0
- package/src/cognition/cache/deliberation.cache.ts +219 -0
- package/src/cognition/cache/fingerprint.ts +120 -0
- package/src/cognition/cache/types.ts +105 -0
- package/src/cognition/config.mirror.entities.ts +109 -1
- package/src/cognition/event.schemas.ts +22 -0
- package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
- package/src/cognition/faculties/episodic.consolidator.ts +59 -3
- package/src/cognition/faculties/executive.engine/commands.ts +189 -14
- package/src/cognition/faculties/executive.engine/context.ts +67 -13
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
- package/src/cognition/faculties/executive.engine/engine.ts +686 -187
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
- package/src/cognition/faculties/executive.engine/facet.ts +89 -26
- package/src/cognition/faculties/executive.engine/gating.ts +14 -14
- package/src/cognition/faculties/executive.engine/parser.ts +21 -1
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
- package/src/cognition/faculties/executive.engine/types.ts +69 -0
- package/src/cognition/faculties/goal.manager.ts +94 -14
- package/src/cognition/faculties/known.entity.tracker.ts +267 -28
- package/src/cognition/faculties/moral.evaluator.ts +8 -3
- package/src/cognition/faculties/persona.consolidator.ts +141 -0
- package/src/cognition/faculties/reputation.tracker.ts +66 -2
- package/src/cognition/faculties/self.model.updater.ts +19 -12
- package/src/cognition/faculties/social.perception.ts +47 -3
- package/src/cognition/faculties/threat.evaluator.ts +7 -0
- package/src/cognition/faculties/working.memory.ts +10 -20
- package/src/cognition/identity.entity.ts +205 -0
- package/src/cognition/index.ts +11 -0
- package/src/cognition/memory/vector.adapter.ts +12 -3
- package/src/cognition/memory/vector.embedder.ts +54 -7
- package/src/cognition/persona.prior.ts +6 -0
- package/src/cognition/senses/audition.engine/engine.ts +404 -46
- package/src/cognition/senses/base.sense.engine.ts +1 -1
- package/src/cognition/senses/index.ts +12 -0
- package/src/cognition/social.identity.ts +273 -0
- package/src/cognition/utilities/token.tracker.ts +246 -98
- package/src/core/orchestrator.ts +38 -0
- package/src/host/boot.ts +78 -22
- package/src/index.ts +35 -0
- package/src/llm/index.ts +415 -97
- package/src/llm/routing.ts +204 -0
- package/src/llm/summarizer.ts +5 -1
- package/src/llm/wire.contracts.ts +19 -0
- package/src/pma/index.ts +67 -53
- package/src/runners/thin-shim.runner.ts +18 -6
- package/src/sdk/will.ts +121 -22
- package/src/stem/assembly.audit.ts +1 -0
- package/src/stem/guards/identity.coherence.ts +17 -6
- package/src/stem/index.ts +82 -5
- package/src/stem/mind.ts +327 -79
- package/src/stem/policy/arbiter.ts +49 -14
- package/src/stem/policy/rule.table.ts +2 -2
- package/src/stem/tracts/effector.controller.ts +56 -9
- package/src/stem/tracts/outbox.writer.ts +40 -2
- 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`;
|
|
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
|
-
// •
|
|
28
|
-
//
|
|
29
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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.
|
|
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
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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 >
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
//
|
|
214
|
-
|
|
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
|
|
221
|
-
//
|
|
222
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
facet.
|
|
272
|
-
this.
|
|
273
|
-
this.
|
|
274
|
-
|
|
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
|
}
|