@mindot/will 0.8.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.
- package/README.md +15 -11
- package/dist/index.d.ts +188 -141
- package/dist/index.js +6985 -3337
- package/dist/index.js.map +1 -1
- package/dist/surface/channels/discord.d.ts +177 -0
- package/dist/surface/channels/discord.js +394 -0
- package/dist/surface/channels/discord.js.map +1 -0
- package/dist/{channels → surface/channels}/whatsapp.d.ts +2 -2
- package/dist/{channels → surface/channels}/whatsapp.js +11 -5
- package/dist/surface/channels/whatsapp.js.map +1 -0
- package/dist/{cli.js → surface/cli.js} +5382 -1573
- package/dist/surface/cli.js.map +1 -0
- package/dist/{mcp → surface/mcp}/effectors.d.ts +1 -1
- package/dist/{mcp → surface/mcp}/effectors.js +10 -4
- package/dist/surface/mcp/effectors.js.map +1 -0
- package/dist/{will-cS6k4uiJ.d.ts → will-evj9_vrd.d.ts} +6658 -5078
- package/package.json +11 -12
- package/src/cognition/agency/consequence.ts +356 -1
- package/src/cognition/agency/conversation.aim.ts +292 -0
- package/src/cognition/agency/engines/action.selector.ts +142 -5
- package/src/cognition/agency/engines/affordance.synthesizer.ts +200 -7
- package/src/cognition/agency/engines/deliberation.engine.ts +76 -3
- package/src/cognition/agency/engines/motor.schema.executor.ts +509 -32
- package/src/cognition/agency/engines/reafference.engine.ts +160 -4
- package/src/cognition/agency/execution.primitives.ts +17 -4
- package/src/cognition/agency/proactive.communicator.ts +19 -3
- package/src/cognition/agency/reconcile.learning.ts +5 -4
- package/src/cognition/agency/restart.ts +66 -0
- package/src/cognition/agency/schemas/innate.ts +96 -1
- package/src/cognition/agency/schemas/repertoire.ts +19 -1
- package/src/cognition/agency/selection.scoring.ts +49 -0
- package/src/cognition/agency/settlement.ts +203 -0
- package/src/cognition/agency/types.ts +73 -1
- 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 +108 -1
- package/src/cognition/event.schemas.ts +22 -8
- package/src/cognition/faculties/affective.blender.ts +21 -4
- package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
- package/src/cognition/faculties/circadian.oscillator.ts +36 -7
- package/src/cognition/faculties/episodic.consolidator.ts +59 -3
- package/src/cognition/faculties/executive.engine/action.record.ts +129 -0
- package/src/cognition/faculties/executive.engine/commands.ts +189 -14
- package/src/cognition/faculties/executive.engine/context.ts +106 -46
- package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
- package/src/cognition/faculties/executive.engine/engine.ts +714 -136
- package/src/cognition/faculties/executive.engine/escalation.buffer.ts +230 -45
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +320 -65
- package/src/cognition/faculties/executive.engine/facet.ts +81 -26
- package/src/cognition/faculties/executive.engine/gating.ts +14 -14
- package/src/cognition/faculties/executive.engine/parser.ts +120 -38
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +331 -38
- package/src/cognition/faculties/executive.engine/types.ts +84 -12
- package/src/cognition/faculties/exteroception.ts +99 -98
- package/src/cognition/faculties/goal.manager.ts +144 -16
- package/src/cognition/faculties/known.entity.tracker.ts +287 -29
- package/src/cognition/faculties/moral.evaluator.ts +8 -3
- package/src/cognition/faculties/persona.consolidator.ts +141 -0
- package/src/cognition/faculties/planning.engine/engine.ts +4 -4
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +1 -1
- package/src/cognition/faculties/reputation.tracker.ts +66 -2
- package/src/cognition/faculties/self.model.updater.ts +19 -12
- package/src/cognition/faculties/semantic.engine/integrator.ts +1 -1
- 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 +16 -21
- package/src/cognition/identity.entity.ts +205 -0
- package/src/cognition/index.ts +7 -0
- package/src/cognition/instruction.handler.ts +1 -1
- package/src/cognition/memory/vector.adapter.ts +12 -3
- package/src/cognition/memory/vector.embedder.ts +45 -4
- package/src/cognition/percept.entity.ts +122 -0
- package/src/cognition/persona.prior.ts +6 -0
- package/src/cognition/sense.boundary.ts +176 -0
- package/src/cognition/senses/audition.engine/engine.ts +492 -53
- package/src/cognition/senses/base.sense.engine.ts +105 -7
- package/src/cognition/senses/index.ts +83 -13
- package/src/cognition/senses/provenance.ts +128 -0
- package/src/cognition/senses/somatosensation.engine.ts +117 -9
- package/src/cognition/social.identity.ts +296 -0
- package/src/cognition/utilities/token.tracker.ts +94 -10
- package/src/core/orchestrator.ts +51 -0
- package/src/index.ts +39 -5
- package/src/llm/gate.ts +48 -0
- package/src/llm/index.ts +127 -33
- package/src/llm/routing.ts +6 -0
- package/src/llm/summarizer.ts +1 -1
- package/src/llm/wire.contracts.ts +57 -0
- package/src/pma/index.ts +68 -54
- package/src/stem/assembly.audit.ts +1 -0
- package/src/stem/guards/identity.coherence.ts +1 -1
- package/src/stem/index.ts +171 -23
- package/src/stem/mind.ts +198 -57
- package/src/stem/policy/arbiter.ts +10 -0
- package/src/stem/profiles/built-in.ts +7 -0
- package/src/{profiles → stem/profiles}/companion.ts +1 -1
- package/src/{profiles → stem/profiles}/company-brain.ts +1 -1
- package/src/{profiles → stem/profiles}/customer-service.ts +1 -1
- package/src/{profiles → stem/profiles}/game-npc.ts +1 -1
- package/src/{profiles → stem/profiles}/index.ts +1 -1
- package/src/{profiles → stem/profiles}/smart-home.ts +1 -1
- package/src/stem/tracts/effector/escalation.lifecycle.ts +239 -0
- package/src/stem/tracts/effector/policy.enforcement.ts +261 -0
- package/src/stem/tracts/effector/types.ts +110 -0
- package/src/stem/tracts/effector.controller.ts +112 -401
- package/src/stem/tracts/outbox.controller.ts +30 -17
- package/src/stem/tracts/outbox.writer.ts +40 -2
- package/src/stem/tracts/sensory.controller.ts +6 -6
- package/src/stem/tracts/session.logger.ts +6 -1
- package/src/stem/tracts/transport/types.ts +20 -1
- package/src/stem/tracts/transport.controller.ts +33 -9
- package/src/surface/channels/discord.ts +609 -0
- package/src/{channels → surface/channels}/roster.ts +1 -1
- package/src/surface/channels/types.ts +136 -0
- package/src/{channels → surface/channels}/whatsapp.ts +22 -10
- package/src/{cli.ts → surface/cli.ts} +14 -9
- package/src/{host → surface/host}/boot.ts +3 -3
- package/src/{host → surface/host}/utterances.ts +2 -2
- package/src/{mcp → surface/mcp}/effectors.ts +19 -6
- package/src/{mcp → surface/mcp}/server.ts +19 -5
- package/src/{sdk → surface/sdk}/will.ts +194 -33
- package/src/{serve → surface/serve}/server.ts +11 -4
- package/src/types.ts +24 -3
- package/dist/channels/discord.d.ts +0 -69
- package/dist/channels/discord.js +0 -193
- package/dist/channels/discord.js.map +0 -1
- package/dist/channels/whatsapp.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/mcp/effectors.js.map +0 -1
- package/src/channels/discord.ts +0 -214
- package/src/channels/types.ts +0 -46
- package/src/cognition/faculties/executive.engine/messages.ts +0 -102
- package/src/extensions/livestream.ext.ts +0 -570
- package/src/extensions/time.ext.ts +0 -339
- package/src/profiles/built-in.ts +0 -7
- package/src/runners/coherence.runner.ts +0 -49
- package/src/runners/outreach.runner.ts +0 -155
- package/src/runners/social.runner.ts +0 -179
- package/src/runners/thin-shim.runner.ts +0 -205
- /package/dist/{cli.d.ts → surface/cli.d.ts} +0 -0
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
* ExecutiveFacet — one facet per entityId, kept alive for the session duration.
|
|
12
12
|
*
|
|
13
13
|
* Architecture:
|
|
14
|
-
* - External call: WillManager.
|
|
14
|
+
* - External call: WillManager.senseText() → audition.sense(TextMessage)
|
|
15
15
|
* - Percept published on bus: senses.audition.percept
|
|
16
|
-
* - Facet
|
|
16
|
+
* - Facet handoffs to the seat: executive.facet.handoff (escalation | undertaking)
|
|
17
17
|
* - GoalManager integration: automatic via executive.facet.progress (bus)
|
|
18
18
|
* - Chunk streaming: via multi-subscriber chunk callbacks (transport + SSE)
|
|
19
19
|
*
|
|
20
20
|
* The master executive is NOT involved in replies — it learns about conversations
|
|
21
21
|
* only via executive.facet.sync events published by the conversation facets.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* It takes initiative when a facet raises an `executive.facet.handoff` — the one
|
|
23
|
+
* channel every facet type uses to hand the singular seat something it owns.
|
|
24
24
|
*
|
|
25
25
|
* FocusSection.outputFormat provides a custom format that re-enables [REPLY]
|
|
26
26
|
* (normally gated out in facet mode) while removing PLANS (conversation
|
|
@@ -60,6 +60,7 @@ import type { ExecutiveEngine } from '#faculties/executive.engine'
|
|
|
60
60
|
import type { ExecutiveFacetHandle, FacetDecision } from '#faculties/executive.engine/facet'
|
|
61
61
|
import { DEFAULT_FACET_AWARENESS, type FocusSection } from '#faculties/executive.engine/prompt.factory'
|
|
62
62
|
import type { ExecutiveOutputFull } from '#faculties/executive.engine/types'
|
|
63
|
+
import { COMMUNICATE_ACTION_TYPES } from '#faculties/executive.engine/commands'
|
|
63
64
|
import type { EpisodicConsolidator } from '#faculties/episodic.consolidator'
|
|
64
65
|
import type { OutboxWriter } from '#stem/tracts/outbox.writer'
|
|
65
66
|
import { GenerativeModel } from '#cognition/generative.model'
|
|
@@ -72,8 +73,13 @@ import type {
|
|
|
72
73
|
SensoryInput,
|
|
73
74
|
LanguagePercept,
|
|
74
75
|
TextMessage,
|
|
76
|
+
Transduced,
|
|
75
77
|
VoiceChunk
|
|
76
78
|
} from '#senses/index'
|
|
79
|
+
import { PERCEPT_SUMMARY_CAP } from '#cognition/percept.entity'
|
|
80
|
+
import { validateFacetHandoff, type HandoffBody } from '#faculties/executive.engine/escalation.buffer'
|
|
81
|
+
import { fnv1a } from '#agency/consequence'
|
|
82
|
+
import type { OutreachResult } from '#agency/engines/motor.schema.executor'
|
|
77
83
|
|
|
78
84
|
// ── Internal types ─────────────────────────────────────────────
|
|
79
85
|
|
|
@@ -82,7 +88,32 @@ interface ConversationDecision {
|
|
|
82
88
|
reply: string
|
|
83
89
|
/** Individual reply bubbles for display (separate SSE chunks). */
|
|
84
90
|
replyBubbles: string[]
|
|
91
|
+
/**
|
|
92
|
+
* The mind DECLARED silence — it considered speaking and chose not to.
|
|
93
|
+
*
|
|
94
|
+
* Distinct from empty bubbles, which also happens when authoring timed out, the
|
|
95
|
+
* facet budget was full, or a second pass deferred to one already in flight.
|
|
96
|
+
* Only this one is an answer; the others are the absence of one, and the
|
|
97
|
+
* executor must keep holding those.
|
|
98
|
+
*/
|
|
99
|
+
withheld?: boolean
|
|
85
100
|
targetEntityId: string
|
|
101
|
+
/**
|
|
102
|
+
* Actions this facet aimed at someone OTHER than the person it is talking to.
|
|
103
|
+
*
|
|
104
|
+
* A conversation facet is the mind talking to ONE person. When it decides mid-
|
|
105
|
+
* conversation to contact a third party ("I'll reach out to FKEM now"), it must
|
|
106
|
+
* not open that channel itself — a facet bound to Fabrice messaging FKEM is the
|
|
107
|
+
* parallel-conversation failure, and it would collide with any facet already
|
|
108
|
+
* talking to FKEM. So the intention is surfaced to the master, which is singular
|
|
109
|
+
* and owns whom the mind contacts.
|
|
110
|
+
*
|
|
111
|
+
* Before this existed, `extractDecision` read `output.actions` only to test for
|
|
112
|
+
* 'escalate' and dropped the rest: a third-party action was delivered nowhere,
|
|
113
|
+
* became no intent, competed in nothing and left no reafference — the mind said
|
|
114
|
+
* it would make contact, believed it had, and nothing ever went out.
|
|
115
|
+
*/
|
|
116
|
+
outwardIntents?: { target: string; gist?: string; reasoning?: string }[]
|
|
86
117
|
newGoals?: ExecutiveOutputFull['newGoals']
|
|
87
118
|
goalsToAbandon?: ExecutiveOutputFull['goalsToAbandon']
|
|
88
119
|
newBeliefs?: ExecutiveOutputFull['newBeliefs']
|
|
@@ -114,12 +145,52 @@ interface CoalesceWindow {
|
|
|
114
145
|
}
|
|
115
146
|
|
|
116
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Split a facet's actions into the ones aimed at the person it is talking to
|
|
150
|
+
* (delivered as the reply, which the [REPLY_TEXT] block already carries) and the
|
|
151
|
+
* ones aimed at someone else (returned here, for the master to own).
|
|
152
|
+
*
|
|
153
|
+
* "Aimed at this person" is matched against both the bound keid and the name the
|
|
154
|
+
* facet was given for them, because a mind writes whichever it is looking at —
|
|
155
|
+
* the trace shows it using `discord:1019…` and `Fabrice` interchangeably within a
|
|
156
|
+
* single decision. Anything else — including a name the mind has heard but never
|
|
157
|
+
* bound to anyone — is outward, and gets carried rather than dropped: whether it
|
|
158
|
+
* can be reached at all is the master's problem to notice, not this partition's.
|
|
159
|
+
*/
|
|
160
|
+
export function partitionOutwardIntents(
|
|
161
|
+
actions: ExecutiveOutputFull['actions'] | undefined,
|
|
162
|
+
boundKeid: string,
|
|
163
|
+
boundName: string,
|
|
164
|
+
): { target: string; gist?: string; reasoning?: string }[] {
|
|
165
|
+
const mine = new Set( [ boundKeid, boundName ].map( s => s.trim().toLowerCase() ).filter( Boolean ) )
|
|
166
|
+
const out: { target: string; gist?: string; reasoning?: string }[] = []
|
|
167
|
+
|
|
168
|
+
for( const action of actions ?? [] ){
|
|
169
|
+
if( !COMMUNICATE_ACTION_TYPES.has( action.type.toLowerCase() ) ) continue
|
|
170
|
+
|
|
171
|
+
const args = ( action.args && typeof action.args === 'object' ? action.args : {} ) as Record<string, unknown>
|
|
172
|
+
const target = [ action.target, args['to'], args['recipient'], args['target'] ]
|
|
173
|
+
.find( v => typeof v === 'string' && v.trim().length > 0 ) as string | undefined
|
|
174
|
+
|
|
175
|
+
// No addressee named at all ⇒ it meant the person in front of it; the
|
|
176
|
+
// [REPLY_TEXT] block is already that reply.
|
|
177
|
+
if( !target || mine.has( target.trim().toLowerCase() ) ) continue
|
|
178
|
+
|
|
179
|
+
const gist = [ args['content'], args['message'], args['text'], args['body'] ]
|
|
180
|
+
.find( v => typeof v === 'string' && v.trim().length > 0 ) as string | undefined
|
|
181
|
+
|
|
182
|
+
out.push({ target: target.trim(), ...( gist ? { gist } : {} ), ...( action.reasoning ? { reasoning: action.reasoning } : {} ) })
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return out
|
|
186
|
+
}
|
|
187
|
+
|
|
117
188
|
// ── Conversation output format ─────────────────────────────────
|
|
118
189
|
// Two-step format: JSON first (private reasoning), then [REPLY_TEXT] (streamed to client).
|
|
119
190
|
// [REPLY_TEXT] is plain prose — no JSON wrapper, no targetEntityId needed.
|
|
120
191
|
// The facet is entity-scoped so the recipient is always the speakerEntityId.
|
|
121
192
|
|
|
122
|
-
const CONVERSATION_OUTPUT_FORMAT = `\
|
|
193
|
+
export const CONVERSATION_OUTPUT_FORMAT = `\
|
|
123
194
|
## Response Format (REQUIRED)
|
|
124
195
|
|
|
125
196
|
Step 1 — JSON object (my private reasoning, optionally in a \`\`\`json code block):
|
|
@@ -145,25 +216,56 @@ Start a new paragraph (blank line) to send a separate chat bubble.
|
|
|
145
216
|
Write [REPLY_TEXT] AFTER the closing \`\`\`. This is the only part the speaker sees — keep it grounded, present.
|
|
146
217
|
Separate multiple messages with a blank line for natural conversational pauses (like separate texts).
|
|
147
218
|
|
|
219
|
+
## Saying nothing
|
|
220
|
+
Silence is a real choice and it is available to me: if I have nothing to say right now — I am
|
|
221
|
+
waiting on them, or speaking again would only repeat myself — I write a [NO_MESSAGE] block
|
|
222
|
+
instead of a [REPLY_TEXT] one, and I put my reason inside it:
|
|
223
|
+
|
|
224
|
+
[NO_MESSAGE]
|
|
225
|
+
Nothing new to add — I am waiting on their answer to what I already asked.
|
|
226
|
+
[/NO_MESSAGE]
|
|
227
|
+
|
|
228
|
+
That is recorded and NEVER sent. Anything between the [REPLY_TEXT] markers IS SENT, so a line
|
|
229
|
+
like "[no message this cycle — waiting for their reply]" does not describe my silence to
|
|
230
|
+
myself, it delivers that sentence to them. If I write both blocks, the silence wins.
|
|
231
|
+
|
|
232
|
+
## Reaching someone who is not in this conversation
|
|
233
|
+
[REPLY_TEXT] is delivered TO THE PERSON I AM TALKING TO, and to nobody else. It has an
|
|
234
|
+
audience, not just a reader. So words meant for a third party do not become a message to
|
|
235
|
+
that third party by being about them — they are handed to the speaker, who reads something
|
|
236
|
+
addressed to someone else, while the person it was actually for never hears it.
|
|
237
|
+
|
|
238
|
+
To reach someone else I name them in an action:
|
|
239
|
+
|
|
240
|
+
{"type": "reach-out", "target": "<their name or id as it appears under '## People I Know'>",
|
|
241
|
+
"args": {"content": "what I want to say to them"}}
|
|
242
|
+
|
|
243
|
+
I am one conversation of a mind that is having several. Opening a channel is not mine to
|
|
244
|
+
do — that action hands the intention to the part of me that owns whom I contact. It is a
|
|
245
|
+
decision, not a delivery: at the moment I write it nothing has reached anyone, and it may
|
|
246
|
+
yet come to nothing. So I never tell the person in front of me that I have contacted
|
|
247
|
+
someone, or that a message is on its way. The most I can truthfully say is that I mean to.
|
|
248
|
+
I keep [REPLY_TEXT] for the person in front of me.
|
|
249
|
+
|
|
148
250
|
## When to use GOALS_NEW (almost always)
|
|
149
251
|
If the speaker requests, mentions, or implies something I should follow through on — embed [GOALS_NEW] in my reasoning.
|
|
150
|
-
This tracks intent across future cycles
|
|
252
|
+
This tracks intent across future cycles on its own.
|
|
151
253
|
|
|
152
254
|
## When to use the escalate action (rare — only for multi-step tasks)
|
|
153
|
-
Use \`{"type": "escalate", "reasoning": "...", "expectedOutcome": "..."}\` in actions ONLY when the request genuinely
|
|
255
|
+
Use \`{"type": "escalate", "reasoning": "...", "expectedOutcome": "..."}\` in actions ONLY when the request genuinely needs a plan I carry out over time rather than an answer I can give now:
|
|
154
256
|
- The task involves multiple steps across future cycles ("build me X", "monitor Y", "set up Z")
|
|
155
257
|
- The request changes my active goal priorities in a significant way
|
|
156
258
|
- I need to coordinate something beyond a single reply
|
|
157
259
|
|
|
158
|
-
**The "reasoning" field on the escalate action becomes the
|
|
260
|
+
**The "reasoning" field on the escalate action becomes the description of the work I pick up.**
|
|
159
261
|
Make it concrete — describe WHAT needs to happen, not just that I am escalating.
|
|
160
262
|
Good: type=escalate, reasoning="User wants weekly mood summaries by email every Monday. Needs: data aggregation, schedule, email delivery.", expectedOutcome="Weekly email delivered."
|
|
161
263
|
Bad: type=escalate, reasoning="Escalating because this is complex."
|
|
162
264
|
|
|
163
265
|
When I escalate:
|
|
164
266
|
1. STILL include a [REPLY_TEXT] that acknowledges the request (e.g. "Got it — I'm on it.")
|
|
165
|
-
2.
|
|
166
|
-
3. Do NOT include a [PLANS] block —
|
|
267
|
+
2. I form and carry out the plan away from this conversation, over the cycles that follow
|
|
268
|
+
3. Do NOT include a [PLANS] block — the planning happens there, not here
|
|
167
269
|
|
|
168
270
|
For simple, single-exchange requests (questions, opinions, short tasks) — do NOT escalate. Just reply.`
|
|
169
271
|
|
|
@@ -270,6 +372,33 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
270
372
|
* other percept uses. Wired to `stateManager.setEntity` in assembleMind().
|
|
271
373
|
*/
|
|
272
374
|
private _memorySink: (( entity: MemoryEntity ) => void) | null = null
|
|
375
|
+
/**
|
|
376
|
+
* Sim tick of the most recent facet decision — the only deterministic clock this
|
|
377
|
+
* off-tick engine has. Stamped from `FacetDecision.tick`, and used to key the
|
|
378
|
+
* conversation records it writes into state.
|
|
379
|
+
*/
|
|
380
|
+
/**
|
|
381
|
+
* Audition does NOT lay down a percept trace (SIGNAL_BOUNDARY P0, step 3).
|
|
382
|
+
*
|
|
383
|
+
* Every other sense does, and a host implementing a new one gets it by
|
|
384
|
+
* default. This is the grandfathered exception, and the reason is measurement
|
|
385
|
+
* rather than principle: audition is the only live sense, and switching it on
|
|
386
|
+
* routes every inbound message to five consumers it has never reached —
|
|
387
|
+
* `action.selector`'s rupture gate (where a fresh high-salience exafferent
|
|
388
|
+
* percept can preempt an awaiting intent), working memory, the executive
|
|
389
|
+
* prompt, novelty, and reafference credit. On a deployed Will that is not a
|
|
390
|
+
* tweak; it is a different mind, and it deserves a measured rollout rather
|
|
391
|
+
* than a line in a refactor.
|
|
392
|
+
*
|
|
393
|
+
* Audition is not trace-less meanwhile: it writes `conversation.received`
|
|
394
|
+
* through its own sink, which is what SocialPerception reads. That record is
|
|
395
|
+
* social — sourceKeid, directedAtSelf, action:'communication' — and is NOT
|
|
396
|
+
* the generic percept the other senses now write. Two different traces for
|
|
397
|
+
* two different readers; this flag turns off only the second.
|
|
398
|
+
*/
|
|
399
|
+
protected readonly tracesPercepts = false
|
|
400
|
+
|
|
401
|
+
private _lastDecisionTick = 0
|
|
273
402
|
/** Speaker attachment strength accessor (0–1) — weights salience by relationship. */
|
|
274
403
|
private _getAttachmentScore: (( entityId: string ) => number) | null = null
|
|
275
404
|
/** Active-goal topic text accessor — for salience topic-overlap. */
|
|
@@ -278,6 +407,8 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
278
407
|
private _inflightInbound = new Map<string, string>()
|
|
279
408
|
/** In-flight thread per entity — stamps chunk envelopes with the current threadId. */
|
|
280
409
|
private _inflightThread = new Map<string, string>()
|
|
410
|
+
/** Targets with an outreach being composed right now — see authorOutreach. */
|
|
411
|
+
private _outreachInFlight = new Set<string>()
|
|
281
412
|
|
|
282
413
|
// ── Assembly wiring ─────────────────────────────────────────
|
|
283
414
|
|
|
@@ -331,11 +462,11 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
331
462
|
|
|
332
463
|
// ── CognitiveEngine ─────────────────────────────────────────
|
|
333
464
|
|
|
334
|
-
/** Override: audition adds the master
|
|
465
|
+
/** Override: audition adds the facet→master handoff to the base percept schema. */
|
|
335
466
|
publishes(): CognitiveEventSchema[] {
|
|
336
467
|
return [
|
|
337
|
-
{ type: 'senses.audition.percept',
|
|
338
|
-
{ type: '
|
|
468
|
+
{ type: 'senses.audition.percept', version: 1, validate: () => null },
|
|
469
|
+
{ type: 'executive.facet.handoff', version: 1, validate: validateFacetHandoff },
|
|
339
470
|
]
|
|
340
471
|
}
|
|
341
472
|
// subscribes() and onCognitiveEvent() inherit the base no-ops (ingest-driven).
|
|
@@ -477,7 +608,7 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
477
608
|
const salience = this._model.observe(`audition.${entityId}`, langEnergy ).salience
|
|
478
609
|
|
|
479
610
|
// ── Percept ────────────────────────────────────────────────
|
|
480
|
-
const
|
|
611
|
+
const transduced: Transduced<LanguagePercept> = {
|
|
481
612
|
domain: 'audition',
|
|
482
613
|
channel: msg.kind,
|
|
483
614
|
content,
|
|
@@ -485,6 +616,10 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
485
616
|
speakerEntityId: entityId,
|
|
486
617
|
threadId,
|
|
487
618
|
digest: this._digests.getDigest( threadId ),
|
|
619
|
+
// What a heard turn amounts to, for readers that do not know this is
|
|
620
|
+
// audition. Bounded, because `summary` renders into the executive prompt
|
|
621
|
+
// and a pasted essay would take the whole percept budget.
|
|
622
|
+
summary: `${ speakerName } said: ${ content }`.slice( 0, PERCEPT_SUMMARY_CAP ),
|
|
488
623
|
salience,
|
|
489
624
|
// Arrival metadata for an EXTERNAL inbound message (network/RPC boundary):
|
|
490
625
|
// no sim clock in scope here and the value is not replayed — wallClock() is
|
|
@@ -495,7 +630,7 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
495
630
|
|
|
496
631
|
// Publish to CognitiveBus — AttentionAllocator et al. can react.
|
|
497
632
|
// publishPercept() (base) is the single emit chokepoint on senses.<domain>.percept.
|
|
498
|
-
this.publishPercept(
|
|
633
|
+
const percept = this.publishPercept<LanguagePercept>( transduced, msg )
|
|
499
634
|
|
|
500
635
|
// ── Update digest with inbound turn ───────────────────────
|
|
501
636
|
this._digests.append( threadId, 'user', content )
|
|
@@ -506,6 +641,16 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
506
641
|
this._inflightInbound.set( entityId, content )
|
|
507
642
|
this._inflightThread.set( entityId, threadId )
|
|
508
643
|
|
|
644
|
+
// Someone spoke to us — record it in state so SOCIAL COGNITION can see it.
|
|
645
|
+
// Until this, an inbound message existed only on the bus and inside a facet:
|
|
646
|
+
// it created no entity, so SocialPerception (whose whole job is to notice
|
|
647
|
+
// people acting toward us) had nothing to scan, never published
|
|
648
|
+
// `interaction.occurred`, and every consumer of that event — reputation,
|
|
649
|
+
// affect, theory-of-mind, attachment, frustration — learned nothing from any
|
|
650
|
+
// conversation the Will ever had. A Will could hold 27 exchanges with someone
|
|
651
|
+
// and still carry familiarity 0, valence 0 for them.
|
|
652
|
+
this._writeReceived( entityId, speakerName, content, threadId )
|
|
653
|
+
|
|
509
654
|
// ── Route to facet, then block the entity queue until the turn resolves ──
|
|
510
655
|
// The turn deferred must be armed BEFORE routing because a synchronous facet
|
|
511
656
|
// (e.g. a test mock) can fire its decision during report().
|
|
@@ -550,6 +695,16 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
550
695
|
this._turnDone.get( entityId )?.()
|
|
551
696
|
}
|
|
552
697
|
|
|
698
|
+
/**
|
|
699
|
+
* True while a turn with this person is still resolving — they spoke and the
|
|
700
|
+
* reply has not landed yet. The agency asks before delivering a self-initiated
|
|
701
|
+
* message (see OutreachAuthor.isSpeakingTo), so one mind does not reach one
|
|
702
|
+
* person down two paths in the same tick.
|
|
703
|
+
*/
|
|
704
|
+
isSpeakingTo( entityId: string ): boolean {
|
|
705
|
+
return this._turnDone.has( entityId )
|
|
706
|
+
}
|
|
707
|
+
|
|
553
708
|
// ── Facet lifecycle ─────────────────────────────────────────
|
|
554
709
|
|
|
555
710
|
private async _routeToFacet( percept: LanguagePercept, speakerName: string ): Promise<boolean> {
|
|
@@ -561,7 +716,10 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
561
716
|
let handle = this._facets.get( percept.speakerEntityId )
|
|
562
717
|
if( !handle ){
|
|
563
718
|
// New conversation session — try to spawn a facet.
|
|
564
|
-
|
|
719
|
+
// Keyed by speaker: one thread of attention per person. The supervisor now
|
|
720
|
+
// owns that guarantee (and carries the thread's reasoning across a reap),
|
|
721
|
+
// so a re-spawn after an idle gap resumes rather than starting cold.
|
|
722
|
+
const result = this._executiveEngine.spawnFacet('conversation', `conversation:${percept.speakerEntityId}`)
|
|
565
723
|
if( result.attention === 'full' || !result.handle ){
|
|
566
724
|
logger.warn(
|
|
567
725
|
`[audition-engine] Executive attention full — ` +
|
|
@@ -691,6 +849,11 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
691
849
|
awareness: [ ...DEFAULT_FACET_AWARENESS, 'plans' ],
|
|
692
850
|
awarenessEntityId: percept.speakerEntityId,
|
|
693
851
|
|
|
852
|
+
// Who this facet is with — reported to the master on every facet sync so the
|
|
853
|
+
// singular seat knows whose conversations these are, not just how many.
|
|
854
|
+
subjectEntityId: percept.speakerEntityId,
|
|
855
|
+
subjectName: speakerName,
|
|
856
|
+
|
|
694
857
|
instructions: [
|
|
695
858
|
'I am in a live conversation with this person. I respond as myself.',
|
|
696
859
|
'I stay grounded in my real memories and feelings — I do not invent experiences I have no record of.',
|
|
@@ -704,15 +867,28 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
704
867
|
// paragraphs (double-newline separated) become separate reply bubbles.
|
|
705
868
|
extractDecision: ( raw: unknown ): ConversationDecision => {
|
|
706
869
|
const output = raw as ExecutiveOutputFull
|
|
707
|
-
|
|
870
|
+
// A declared silence suppresses the words and NOTHING else — the goals,
|
|
871
|
+
// beliefs and entity updates below are things the mind worked out from
|
|
872
|
+
// what it heard, and they are true whether or not it answers. Discarding
|
|
873
|
+
// them with the reply would make choosing silence cost the mind its
|
|
874
|
+
// learning, which is a reason not to choose it.
|
|
875
|
+
const silent = output.noMessage !== undefined
|
|
876
|
+
if( silent )
|
|
877
|
+
logger.info(`[audition-engine] chose silence toward ${ speakerName ?? percept.speakerEntityId }: ${ output.noMessage!.slice( 0, 120 ) }`)
|
|
878
|
+
const rawReply = silent ? '' : ( output.replyText?.trim() ?? '')
|
|
708
879
|
const bubbles = rawReply.split( /\n{2,}/ )
|
|
709
880
|
.map( b => b.trim() )
|
|
710
881
|
.filter( Boolean )
|
|
711
882
|
|
|
883
|
+
const outwardIntents = partitionOutwardIntents(
|
|
884
|
+
output.actions, percept.speakerEntityId, speakerName,
|
|
885
|
+
)
|
|
886
|
+
|
|
712
887
|
return {
|
|
713
888
|
reply: bubbles.join('\n'),
|
|
714
889
|
replyBubbles: bubbles,
|
|
715
890
|
targetEntityId: percept.speakerEntityId,
|
|
891
|
+
...( outwardIntents.length > 0 ? { outwardIntents } : {} ),
|
|
716
892
|
newGoals: output.newGoals,
|
|
717
893
|
goalsToAbandon: output.goalsToAbandon,
|
|
718
894
|
newBeliefs: output.newBeliefs,
|
|
@@ -734,33 +910,97 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
734
910
|
* (MotorSchemaExecutor) to deliver through the proactive communicate path; empty
|
|
735
911
|
* when no executive is attached or the facet budget is full (caller then awaits).
|
|
736
912
|
*/
|
|
737
|
-
async authorOutreach( entityId: string, entityName: string, gist?: string ): Promise<
|
|
738
|
-
if( !this._executiveEngine ) return []
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
913
|
+
async authorOutreach( entityId: string, entityName: string, gist?: string ): Promise<OutreachResult> {
|
|
914
|
+
if( !this._executiveEngine ) return { bubbles: [] }
|
|
915
|
+
|
|
916
|
+
// One authoring pass per person at a time.
|
|
917
|
+
//
|
|
918
|
+
// This was unguarded, and the agency can hold more than one intent toward the
|
|
919
|
+
// same target at once (two undertakings, or an undertaking plus a self-initiated
|
|
920
|
+
// reach). Each one spawned its own transient facet, each facet independently
|
|
921
|
+
// composed a message, and both were delivered — the same question asked twice,
|
|
922
|
+
// reworded, seconds apart. The executor's idempotence was keyed by INTENT id,
|
|
923
|
+
// which cannot see that two intents mean one conversation.
|
|
924
|
+
//
|
|
925
|
+
// A concurrent second call returns empty rather than waiting: its intent stays
|
|
926
|
+
// 'awaiting' and comes back round once this pass has landed and satiation has
|
|
927
|
+
// had a chance to read it, which is the outcome we want anyway.
|
|
928
|
+
if( this._outreachInFlight.has( entityId ) ){
|
|
929
|
+
logger.info(`[audition-engine] already composing an outreach to ${ entityId } — not opening a second`)
|
|
930
|
+
return { bubbles: [] }
|
|
743
931
|
}
|
|
744
|
-
const handle = spawned.handle
|
|
745
932
|
|
|
746
|
-
|
|
933
|
+
// Already talking to them? Then this is not a second thread — it is a thing to
|
|
934
|
+
// say in the one that is open, and it must be said BY that thread.
|
|
935
|
+
//
|
|
936
|
+
// A transient facet composing in parallel cannot see the live conversation: not
|
|
937
|
+
// the thread digest, not what was said two minutes ago, not the thinking the
|
|
938
|
+
// open facet has been doing about this person. So the mind asked the same
|
|
939
|
+
// question it had already asked, in different words, while the answer was
|
|
940
|
+
// sitting in a thread it was not reading. Routing through the open facet costs
|
|
941
|
+
// nothing extra — the focus rides on the REPORT (see FacetReport.focus), so the
|
|
942
|
+
// conversation's own standing focus is never touched and the next inbound turn
|
|
943
|
+
// resumes exactly where it was.
|
|
944
|
+
const openThread = this._executiveEngine.facetFor(`conversation:${ entityId }`)
|
|
945
|
+
const handle = openThread ?? ( () => {
|
|
946
|
+
// Nobody home — a transient authoring facet, deliberately NOT supervisor-keyed:
|
|
947
|
+
// it is exactly what the mind can most afford to evict under pressure, and a
|
|
948
|
+
// key would move it into the protected tier alongside live conversations.
|
|
949
|
+
const spawned = this._executiveEngine!.spawnFacet('outreach')
|
|
950
|
+
if( spawned.attention === 'full' || !spawned.handle ){
|
|
951
|
+
logger.warn(`[audition-engine] facet budget full — cannot author outreach to ${ entityId }`)
|
|
952
|
+
return undefined
|
|
953
|
+
}
|
|
954
|
+
return spawned.handle
|
|
955
|
+
} )()
|
|
956
|
+
|
|
957
|
+
if( !handle ) return { bubbles: [] }
|
|
958
|
+
|
|
959
|
+
if( openThread )
|
|
960
|
+
logger.info(`[audition-engine] composing outreach to ${ entityId } inside the open conversation (${ openThread.facetId })`)
|
|
961
|
+
|
|
962
|
+
// What we have already said to each other, when there IS an open thread. Without
|
|
963
|
+
// it the mind opens with a question it asked four minutes ago.
|
|
964
|
+
const digest = openThread ? this._digests.getDigest( this._inflightThread.get( entityId ) ?? entityId ) : ''
|
|
965
|
+
|
|
966
|
+
const outreachFocus: FocusSection = ({
|
|
747
967
|
title: 'Reaching out',
|
|
748
968
|
function: 'outreach',
|
|
749
969
|
content: [
|
|
750
|
-
|
|
751
|
-
|
|
970
|
+
openThread
|
|
971
|
+
? `I am already in conversation with ${ entityName } (id: ${ entityId }), and there is something I have decided to say to them now — unprompted, not an answer to anything they asked.`
|
|
972
|
+
: `I have decided, on my own initiative, to reach out to ${ entityName } (id: ${ entityId }).`,
|
|
973
|
+
openThread
|
|
974
|
+
? 'This continues that conversation. I do not re-introduce myself and I do not ask again for something already answered above.'
|
|
975
|
+
: 'No one prompted this — I am choosing to make contact now.',
|
|
976
|
+
digest,
|
|
752
977
|
gist ? `What is on my mind: ${ gist }` : '',
|
|
978
|
+
// The gist is what the MASTER framed, and the master was not talking to
|
|
979
|
+
// them — so it refers to people in the third person, including sometimes
|
|
980
|
+
// the very person about to read it. Observed live: "Fabrice says the
|
|
981
|
+
// server issue is fixed now and he should look into the logs", addressed
|
|
982
|
+
// TO him. The words are mine to choose; the gist is only what I mean.
|
|
983
|
+
gist ? `That is my sense of it, not my words to them — I am speaking to ${ entityName } directly, so I say it the way I would say it to their face.` : '',
|
|
753
984
|
].filter( Boolean ).join('\n'),
|
|
754
985
|
recallQuery: gist ?? entityName,
|
|
755
986
|
awareness: [ ...DEFAULT_FACET_AWARENESS, 'plans' ],
|
|
756
987
|
awarenessEntityId: entityId,
|
|
988
|
+
subjectEntityId: entityId,
|
|
989
|
+
subjectName: entityName,
|
|
757
990
|
instructions:
|
|
758
991
|
'Considering who I am, my goals, and how I feel, I say what I genuinely want to say to ' +
|
|
759
992
|
'them now. I speak as myself; I stay grounded in my real memories — I do not invent experiences ' +
|
|
760
993
|
'I have no record of.',
|
|
761
994
|
outputFormat: CONVERSATION_OUTPUT_FORMAT,
|
|
762
995
|
extractDecision: ( raw: unknown ): ConversationDecision => {
|
|
763
|
-
const output
|
|
996
|
+
const output = raw as ExecutiveOutputFull
|
|
997
|
+
// A declared silence beats anything else in the response. Unprompted
|
|
998
|
+
// speech is the one case where saying nothing must be cheaper than
|
|
999
|
+
// saying something — nobody is waiting on this.
|
|
1000
|
+
if( output.noMessage !== undefined ){
|
|
1001
|
+
logger.info(`[audition-engine] chose not to reach out to ${ entityName }: ${ output.noMessage.slice( 0, 120 ) }`)
|
|
1002
|
+
return { reply: '', replyBubbles: [], withheld: true, targetEntityId: entityId, requiresMasterAttention: false }
|
|
1003
|
+
}
|
|
764
1004
|
const rawReply = output.replyText?.trim() ?? ''
|
|
765
1005
|
const bubbles = rawReply.split( /\n{2,}/ ).map( b => b.trim() ).filter( Boolean )
|
|
766
1006
|
return { reply: bubbles.join('\n'), replyBubbles: bubbles, targetEntityId: entityId, requiresMasterAttention: false }
|
|
@@ -770,24 +1010,59 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
770
1010
|
// report() only QUEUES the facet's reasoning; the authored bubbles arrive LATER
|
|
771
1011
|
// via the subscription. So wait for the DECISION (not report's resolution), with
|
|
772
1012
|
// a safety timeout, then tear the transient facet down.
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
() => {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1013
|
+
this._outreachInFlight.add( entityId )
|
|
1014
|
+
try {
|
|
1015
|
+
const authored = await new Promise<OutreachResult>( resolve => {
|
|
1016
|
+
let settled = false
|
|
1017
|
+
let unsub: () => void = () => {}
|
|
1018
|
+
let timer: ReturnType<typeof setTimeout>
|
|
1019
|
+
const done = ( r: OutreachResult ): void => { if( settled ) return; settled = true; clearTimeout( timer ); unsub(); resolve( r ) }
|
|
1020
|
+
timer = setTimeout(
|
|
1021
|
+
() => { logger.warn(`[audition-engine] outreach authoring timed out for ${ entityId }`); done( { bubbles: [] } ) },
|
|
1022
|
+
60_000, // generous: the facet LLM authors in ~8–18s
|
|
1023
|
+
)
|
|
1024
|
+
// ONLY this report's decision. Sharing a live conversation facet means its
|
|
1025
|
+
// ordinary reply decisions arrive on the same subscription, and resolving
|
|
1026
|
+
// on one of those would hand the human's reply back as if the mind had
|
|
1027
|
+
// composed it unprompted — and deliver it twice.
|
|
1028
|
+
unsub = handle.subscribe( d => {
|
|
1029
|
+
if( d.respondingToType !== 'outreach') return
|
|
1030
|
+
const decision = d.decision as ConversationDecision
|
|
1031
|
+
// A facet reasoned and its decision arrived — `answered`, whatever it
|
|
1032
|
+
// holds. That is what separates an empty ANSWER from the empty every
|
|
1033
|
+
// other exit on this method returns, and it is the only place on the
|
|
1034
|
+
// method that can honestly say so.
|
|
1035
|
+
done( {
|
|
1036
|
+
bubbles: decision.replyBubbles ?? [],
|
|
1037
|
+
withheld: decision.withheld === true,
|
|
1038
|
+
answered: true,
|
|
1039
|
+
} )
|
|
1040
|
+
} )
|
|
1041
|
+
// The focus rides the REPORT, so a shared conversation facet keeps its own
|
|
1042
|
+
// standing focus and its next inbound turn resumes untouched.
|
|
1043
|
+
Promise.resolve( handle.report({ type: 'outreach', payload: { entityId, gist }, focus: outreachFocus }) ).catch( err => {
|
|
1044
|
+
logger.warn(`[audition-engine] outreach report failed for ${ entityId }: ${ ( err as Error ).message }`)
|
|
1045
|
+
done( { bubbles: [] } )
|
|
1046
|
+
} )
|
|
786
1047
|
} )
|
|
787
|
-
} )
|
|
788
1048
|
|
|
789
|
-
|
|
790
|
-
|
|
1049
|
+
// Only tear down what we opened. Destroying a borrowed conversation facet
|
|
1050
|
+
// would end the conversation as a side effect of speaking in it.
|
|
1051
|
+
if( !openThread ) handle.destroy()
|
|
1052
|
+
return authored
|
|
1053
|
+
}
|
|
1054
|
+
catch( err ){
|
|
1055
|
+
// A facet that throws is a pass that produced no words — the same outcome as
|
|
1056
|
+
// the timeout, and the caller's contract is already "empty means I could not
|
|
1057
|
+
// author". Letting it escape would reject inside MotorSchemaExecutor's
|
|
1058
|
+
// fire-and-forget authoring chain instead.
|
|
1059
|
+
logger.warn(`[audition-engine] outreach authoring failed for ${ entityId }: ${ ( err as Error ).message }`)
|
|
1060
|
+
return { bubbles: [] }
|
|
1061
|
+
}
|
|
1062
|
+
// finally{} on every path — the timeout resolves empty rather than throwing, but
|
|
1063
|
+
// a destroy() or report() that throws must not leave this person permanently
|
|
1064
|
+
// un-reachable by leaving the guard set.
|
|
1065
|
+
finally { this._outreachInFlight.delete( entityId ) }
|
|
791
1066
|
}
|
|
792
1067
|
|
|
793
1068
|
// ── Conversation memory (Section 5) ─────────────────────────
|
|
@@ -801,6 +1076,116 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
801
1076
|
* (Section 1.2) routes ingest through the tick loop. The entity carries no
|
|
802
1077
|
* wall-clock timestamp — `setEntity` stamps createdAt/tick from the sim clock.
|
|
803
1078
|
*/
|
|
1079
|
+
/**
|
|
1080
|
+
* The inbound as a social signal in state — mirror of `conversation.sent`.
|
|
1081
|
+
*
|
|
1082
|
+
* Shaped for `SocialPerception._scanSocialSignals`, which reads `sourceKeid` for
|
|
1083
|
+
* who acted and `directedAtSelf` for whether it was aimed at us. Valence is left
|
|
1084
|
+
* UNSET on purpose: the words have not been appraised yet, and guessing a number
|
|
1085
|
+
* here would feed reputation and affect a sentiment nobody measured. Absent, the
|
|
1086
|
+
* scanner falls back to its neutral default, so the Will learns *that* someone
|
|
1087
|
+
* engaged (familiarity, recency, reliability) without inventing how it felt.
|
|
1088
|
+
*/
|
|
1089
|
+
/**
|
|
1090
|
+
* A durable, deterministic id for a conversation record.
|
|
1091
|
+
*
|
|
1092
|
+
* `<prefix>-<entity>-<tick>-<hash of the words>`. Every part earns its place:
|
|
1093
|
+
* • entity — whose conversation this is;
|
|
1094
|
+
* • tick — WHEN, from the sim clock, which resumes from the snapshot and so
|
|
1095
|
+
* keeps rising across restarts;
|
|
1096
|
+
* • hash — which utterance, so two things said to one person on one tick stay
|
|
1097
|
+
* two records.
|
|
1098
|
+
*
|
|
1099
|
+
* What it replaces was `<prefix>-<entity>-<N>` with N a process-local counter.
|
|
1100
|
+
* It restarted at 1 on every boot, so each session OVERWROTE the previous
|
|
1101
|
+
* session's records of the same person — a mind that had spoken with someone
|
|
1102
|
+
* across four restarts held one session's worth of evidence that it ever had.
|
|
1103
|
+
* Found by diffing a live snapshot against the Discord transcript it came from:
|
|
1104
|
+
* `conv-sent-reply-discord:1019…-1` held that morning's greeting, and every
|
|
1105
|
+
* earlier conversation keyed to the same id was simply gone.
|
|
1106
|
+
*
|
|
1107
|
+
* No wallClock: these ids live in state, and a wall-clock id makes the recorded
|
|
1108
|
+
* and replayed runs diverge (R2).
|
|
1109
|
+
*/
|
|
1110
|
+
private _sentKey( prefix: string, entityId: string, words: string ): string {
|
|
1111
|
+
return `${ prefix }-${ entityId }-${ this._lastDecisionTick }-${ fnv1a( words ) }`
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
private _writeReceived( entityId: string, speakerName: string | undefined, content: string, threadId: string ): void {
|
|
1115
|
+
if( !this._memorySink ) return
|
|
1116
|
+
// Never wallClock(): this entity LIVES IN STATE, so a wall-clock id makes the
|
|
1117
|
+
// recorded and replayed runs diverge (R2). Observed as a replay consuming 17 of
|
|
1118
|
+
// 18 recorded completions — different ids meant different percepts meant a
|
|
1119
|
+
// different executive firing schedule.
|
|
1120
|
+
//
|
|
1121
|
+
// But nor a process-local counter, which is what this was. `conv-received-<id>-N`
|
|
1122
|
+
// restarted at N=1 on every boot, so each session silently OVERWROTE the last
|
|
1123
|
+
// session's records of the same person. A mind that had spoken with someone
|
|
1124
|
+
// across four restarts held one session's worth of evidence that it ever had.
|
|
1125
|
+
// See _sentKey.
|
|
1126
|
+
this._memorySink({
|
|
1127
|
+
id: this._sentKey('conv-received', entityId, content ),
|
|
1128
|
+
type: 'conversation.received',
|
|
1129
|
+
metadata: {
|
|
1130
|
+
sourceKeid: entityId,
|
|
1131
|
+
sourceName: speakerName,
|
|
1132
|
+
directedAtSelf: true, // an inbound turn is addressed to us by definition
|
|
1133
|
+
action: 'communication',
|
|
1134
|
+
preview: content.slice( 0, 140 ),
|
|
1135
|
+
chars: content.length,
|
|
1136
|
+
...( threadId ? { threadId } : {} ),
|
|
1137
|
+
},
|
|
1138
|
+
})
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Record that the mind SPOKE to someone, mirroring `_writeReceived`.
|
|
1143
|
+
*
|
|
1144
|
+
* Only ProactiveCommunicator wrote `conversation.sent`, so a reply — which is
|
|
1145
|
+
* most of what a Will says — left no durable trace of having spoken. Everything
|
|
1146
|
+
* that asks "have I already said something to them?" was therefore blind to
|
|
1147
|
+
* conversation: satiation could not damp repeating a relay delivered as a reply,
|
|
1148
|
+
* and an undertaking discharged inside a conversation stayed forever unkept,
|
|
1149
|
+
* which is exactly how the same message went out again and again.
|
|
1150
|
+
*
|
|
1151
|
+
* Speaking is speaking, whichever path carried it.
|
|
1152
|
+
*/
|
|
1153
|
+
private _writeSent(
|
|
1154
|
+
entityId: string,
|
|
1155
|
+
entityName: string | undefined,
|
|
1156
|
+
bubbles: string[],
|
|
1157
|
+
/**
|
|
1158
|
+
* Outbox ids for these bubbles — the ONLY thing that lets a later delivery
|
|
1159
|
+
* ack find this record (`OutboxController.confirmDelivery` correlates on
|
|
1160
|
+
* `outboxMessageIds`, there is no other key).
|
|
1161
|
+
*
|
|
1162
|
+
* Omitted, this record could never be marked delivered. Every reply the mind
|
|
1163
|
+
* ever made carried `delivered` unset, forever — so a mind asking itself "did
|
|
1164
|
+
* that land?" found no answer for anything it had SAID, while the answer was
|
|
1165
|
+
* recorded faithfully for everything it had initiated. Silence read exactly
|
|
1166
|
+
* like failure, and it re-sent. The proactive path stored these from the
|
|
1167
|
+
* start; the reply path was simply never given them.
|
|
1168
|
+
*/
|
|
1169
|
+
outboxMessageIds?: string[],
|
|
1170
|
+
): void {
|
|
1171
|
+
if( !this._memorySink || bubbles.length === 0 ) return
|
|
1172
|
+
this._memorySink({
|
|
1173
|
+
id: this._sentKey('conv-sent-reply', entityId, bubbles.join('\n') ),
|
|
1174
|
+
type: 'conversation.sent',
|
|
1175
|
+
metadata: {
|
|
1176
|
+
targetEntityId: entityId,
|
|
1177
|
+
targetEntityName: entityName,
|
|
1178
|
+
messageCount: bubbles.length,
|
|
1179
|
+
preview: bubbles[0]?.slice( 0, 100 ) ?? '',
|
|
1180
|
+
effectorName: 'text',
|
|
1181
|
+
source: 'audition-facet',
|
|
1182
|
+
tick: this._lastDecisionTick,
|
|
1183
|
+
delivered: false,
|
|
1184
|
+
...( outboxMessageIds?.length ? { outboxMessageIds } : {} ),
|
|
1185
|
+
},
|
|
1186
|
+
})
|
|
1187
|
+
}
|
|
1188
|
+
|
|
804
1189
|
private _persistExchangeMemory( entityId: string, threadId: string, reply: string, confidence: number, entityName?: string ): void {
|
|
805
1190
|
const inbound = this._inflightInbound.get( entityId ) ?? ''
|
|
806
1191
|
this._inflightInbound.delete( entityId )
|
|
@@ -832,6 +1217,19 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
832
1217
|
// facet's spawn-time thread — correct for an entity that spans threads (§2).
|
|
833
1218
|
const threadId = this._inflightThread.get( entityId ) ?? ''
|
|
834
1219
|
|
|
1220
|
+
// The sim tick this was reasoned at — the only deterministic clock an off-tick
|
|
1221
|
+
// engine has, and what the conversation-record ids are keyed on.
|
|
1222
|
+
this._lastDecisionTick = decision.tick ?? this._lastDecisionTick
|
|
1223
|
+
|
|
1224
|
+
// An outreach composed INSIDE this conversation (authorOutreach borrowing the
|
|
1225
|
+
// open facet) lands here too, because the session subscription is facet-wide.
|
|
1226
|
+
// Its words belong to `authorOutreach`, which returns them to the agency for
|
|
1227
|
+
// delivery through the proactive path; delivering them here as well would send
|
|
1228
|
+
// the same message twice and answer a turn nobody took. `_endTurn` is not
|
|
1229
|
+
// called either — an outreach is not a turn, and releasing the queue here would
|
|
1230
|
+
// let the next inbound start while a real turn was still in flight.
|
|
1231
|
+
if( decision.respondingToType === 'outreach') return
|
|
1232
|
+
|
|
835
1233
|
// finally{} releases the entity's serial turn queue on EVERY exit path —
|
|
836
1234
|
// reply delivered, reply suppressed, or escalation only. Without this a
|
|
837
1235
|
// suppressed-reply early-return would stall the queue until the safety timeout.
|
|
@@ -875,6 +1273,8 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
875
1273
|
pushToOutbox: !viaTransport,
|
|
876
1274
|
})
|
|
877
1275
|
|
|
1276
|
+
this._writeSent( entityId, d.targetEntityId, d.replyBubbles, ids )
|
|
1277
|
+
|
|
878
1278
|
if( viaTransport )
|
|
879
1279
|
logger.info(
|
|
880
1280
|
`[audition-engine] Reply emitted via transport for ${entityId} ` +
|
|
@@ -892,22 +1292,61 @@ export class AuditionEngine extends BaseSenseEngine {
|
|
|
892
1292
|
)
|
|
893
1293
|
}
|
|
894
1294
|
|
|
895
|
-
// ──
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1295
|
+
// ── Outward intentions ────────────────────────────────────
|
|
1296
|
+
// The facet decided, mid-conversation, to say something to someone ELSE.
|
|
1297
|
+
// It does not do that itself (see ConversationDecision.outwardIntents) — it
|
|
1298
|
+
// hands the intention to the master, which is singular and owns whom the
|
|
1299
|
+
// mind contacts. The master perceives it as an undertaking it made and
|
|
1300
|
+
// decides whether it still means it; nothing here forces the contact.
|
|
1301
|
+
// `executive.facet.handoff` is the ONE channel every facet type uses to hand
|
|
1302
|
+
// the master something. It replaced `audition.task.signal`, which was named
|
|
1303
|
+
// and typed for this engine alone — see EscalationBuffer.
|
|
1304
|
+
const handoff = ( body: HandoffBody ): void => {
|
|
1305
|
+
this._bus?.publish({
|
|
1306
|
+
type: 'executive.facet.handoff',
|
|
901
1307
|
version: 1,
|
|
902
1308
|
sourceEngine: this.name,
|
|
903
1309
|
salience: 0.9,
|
|
904
1310
|
payload: {
|
|
905
|
-
|
|
1311
|
+
facetId: decision.facetId,
|
|
1312
|
+
subjectEntityId: entityId,
|
|
1313
|
+
...( d.targetEntityId ? { subjectName: d.targetEntityId } : {} ),
|
|
906
1314
|
threadId,
|
|
907
|
-
|
|
908
|
-
|
|
1315
|
+
confidence: decision.confidence,
|
|
1316
|
+
// No `tick` — this engine runs off-tick and has no honest sim clock of
|
|
1317
|
+
// its own. The master stamps it from the tick the handoff ARRIVES on,
|
|
1318
|
+
// which is within one tick of when it was formed and, crucially, is a
|
|
1319
|
+
// real clock reading rather than the last time the master happened to run.
|
|
1320
|
+
body,
|
|
909
1321
|
}
|
|
910
1322
|
})
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
if( d.outwardIntents?.length )
|
|
1326
|
+
for( const intent of d.outwardIntents ){
|
|
1327
|
+
logger.info(
|
|
1328
|
+
`[audition-engine] Outward intent from ${entityId}'s facet → ${intent.target} ` +
|
|
1329
|
+
`(handing to master; the facet does not open that channel itself)`
|
|
1330
|
+
)
|
|
1331
|
+
handoff({
|
|
1332
|
+
kind: 'undertaking',
|
|
1333
|
+
// The commitment in the mind's own words. A contact is one KIND of
|
|
1334
|
+
// promise, not the only kind the tract can carry — see
|
|
1335
|
+
// UndertakingHandoff. This producer only makes contact-shaped ones,
|
|
1336
|
+
// because a facet declares an outward intent by naming a
|
|
1337
|
+
// communicate action; anything else it means to follow through on
|
|
1338
|
+
// goes through [GOALS_NEW], which the format already asks for.
|
|
1339
|
+
what: `reach ${ intent.target }`,
|
|
1340
|
+
target: intent.target,
|
|
1341
|
+
reasoning: intent.reasoning ?? '',
|
|
1342
|
+
...( intent.gist ? { gist: intent.gist } : {} ),
|
|
1343
|
+
})
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// ── Master escalation signal ──────────────────────────────
|
|
1347
|
+
// Published when the facet emits an 'escalate' action type.
|
|
1348
|
+
if( d.requiresMasterAttention )
|
|
1349
|
+
handoff({ kind: 'escalation', reasoning: decision.reasoning })
|
|
911
1350
|
}
|
|
912
1351
|
finally {
|
|
913
1352
|
// Release the entity's turn queue so the next message can be processed.
|