@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
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { logger } from '#core/logger'
|
|
26
26
|
import type {
|
|
27
|
-
Duration, Tick, SimulationContext,
|
|
27
|
+
Duration, Tick, SimulationContext, SimulationEvent,
|
|
28
28
|
ReadonlySimulationState, StateCommands, EntityInput,
|
|
29
29
|
} from '#core/types'
|
|
30
30
|
import type { CognitiveBus } from '#cognition/bus'
|
|
@@ -39,10 +39,21 @@ import type { ActionRequest, ActionResult } from '#types'
|
|
|
39
39
|
import { INNATE_SCHEMAS } from '#agency/schemas/innate'
|
|
40
40
|
import { enact, type Enaction } from '#agency/execution.primitives'
|
|
41
41
|
import { revokedIntentIds, revocationId, staleRevocationIds } from '#agency/revocation'
|
|
42
|
+
import { addressesOf } from '#cognition/social.identity'
|
|
43
|
+
import { lastAnsweredByEntity } from '#agency/conversation.aim'
|
|
42
44
|
import {
|
|
43
|
-
CONSEQUENCE_TYPE, CONSEQUENCE_TTL_TICKS,
|
|
44
|
-
consequenceEntity, fnv1a, paramsKey,
|
|
45
|
-
|
|
45
|
+
CONSEQUENCE_TYPE, CONSEQUENCE_TTL_TICKS, ENACTED_TYPE,
|
|
46
|
+
consequenceEntity, enactedEntity, fnv1a, paramsKey, spokenAtByEntity } from '#agency/consequence'
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* How long a durable enaction record is kept. Generous against the satiation
|
|
50
|
+
* window (`repeatWindowTicks`, 60 by default) rather than equal to it, because
|
|
51
|
+
* the window is persona-tunable and read in the synthesizer, not here — a
|
|
52
|
+
* retention shorter than some tenant's window would silently reintroduce the
|
|
53
|
+
* very gap this record exists to close. Past this the satiation term is 0
|
|
54
|
+
* regardless, so keeping it longer buys nothing.
|
|
55
|
+
*/
|
|
56
|
+
const ENACTED_RETENTION_TICKS = 600
|
|
46
57
|
|
|
47
58
|
/** Ticks an async (communicate/external) intent may stay 'awaiting' before it is
|
|
48
59
|
* abandoned. Exported: the ReafferenceEngine's sensory-confirmation path (P5)
|
|
@@ -71,11 +82,48 @@ interface Intent {
|
|
|
71
82
|
*/
|
|
72
83
|
planId?: string
|
|
73
84
|
planStepId?: string
|
|
85
|
+
/**
|
|
86
|
+
* The entity that evoked this — for an ideomotor winner, the `ideomotor.intent`
|
|
87
|
+
* the executive wrote. Read so enaction can retire it (see _dischargeWill).
|
|
88
|
+
*/
|
|
89
|
+
evokedBy?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What an authoring pass produced.
|
|
94
|
+
*
|
|
95
|
+
* `bubbles` empty is AMBIGUOUS on its own — it is also what a timed-out facet, a
|
|
96
|
+
* full facet budget, and a pass deferring to one already in flight all return.
|
|
97
|
+
* `answered` is what disambiguates it: whether a facet actually reasoned and came
|
|
98
|
+
* back. An empty answer IS an answer; an empty non-answer means the pass never
|
|
99
|
+
* happened and is worth asking for again.
|
|
100
|
+
*/
|
|
101
|
+
export interface OutreachResult {
|
|
102
|
+
bubbles: string[]
|
|
103
|
+
/** The mind considered speaking and declared silence — the strongest answer. */
|
|
104
|
+
withheld?: boolean
|
|
105
|
+
/**
|
|
106
|
+
* A facet reasoned and its decision came back, whatever it held. Absent means
|
|
107
|
+
* no pass ran at all (no executive, budget full, deferring to one in flight,
|
|
108
|
+
* timed out, threw) — only THAT is worth asking again for.
|
|
109
|
+
*
|
|
110
|
+
* Without this the caller could not tell the two apart, so it re-asked on
|
|
111
|
+
* every tick. Observed live: nineteen authoring passes for one outreach, each
|
|
112
|
+
* returning an empty answer, none of them delivering a word, and the person
|
|
113
|
+
* she had decided to contact never heard from her.
|
|
114
|
+
*/
|
|
115
|
+
answered?: boolean
|
|
74
116
|
}
|
|
75
117
|
|
|
76
118
|
/** Authors the words for a self-initiated communicate the agency selected (no inbound triggered it). */
|
|
77
119
|
export interface OutreachAuthor {
|
|
78
|
-
|
|
120
|
+
/** An array return is still honoured — it reads as "no words, and I am not saying why". */
|
|
121
|
+
authorOutreach( entityId: string, entityName: string, gist?: string ): Promise<string[] | OutreachResult>
|
|
122
|
+
/**
|
|
123
|
+
* True while a turn with this person is still resolving — they spoke and the
|
|
124
|
+
* reply has not landed yet. Optional: an author that cannot say is not blocking.
|
|
125
|
+
*/
|
|
126
|
+
isSpeakingTo?( entityId: string ): boolean
|
|
79
127
|
}
|
|
80
128
|
|
|
81
129
|
export class MotorSchemaExecutor implements CognitiveEngine {
|
|
@@ -88,6 +136,46 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
88
136
|
private _grants: AccessGrants | null = null
|
|
89
137
|
private _bus: CognitiveBus | null = null
|
|
90
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Two-phase outreach authoring. A facet cannot be awaited from inside a tick:
|
|
141
|
+
* `ExecutiveFacet.report()` only QUEUES in tick-discipline mode and the reasoning
|
|
142
|
+
* launches from `pump()`, which the ExecutiveEngine calls once per tick — so an
|
|
143
|
+
* in-tick `await` blocks the very loop that would produce the answer. Observed
|
|
144
|
+
* live: a 61s freeze of the whole mind inside one tick, then an empty result.
|
|
145
|
+
* (It passes unit tests because bare facets have no inbox and author inline.)
|
|
146
|
+
*
|
|
147
|
+
* So `_deliver` REQUESTS words and returns false (the intent holds 'awaiting'),
|
|
148
|
+
* the facet answers off-tick, and a later tick delivers. Process-local by design:
|
|
149
|
+
* an authoring call in flight cannot survive a restart, and the intent would
|
|
150
|
+
* simply re-request.
|
|
151
|
+
*/
|
|
152
|
+
private _authoring = new Set<string>()
|
|
153
|
+
private _authored = new Map<string, string[]>()
|
|
154
|
+
/**
|
|
155
|
+
* Intents whose facet considered speaking and chose NOT to.
|
|
156
|
+
*
|
|
157
|
+
* A declined outreach used to resolve by rotting: no words arrived, so the
|
|
158
|
+
* intent sat 'awaiting' until AWAIT_TIMEOUT abandoned it as a FAILURE — and
|
|
159
|
+
* reafference folded that into `reach-out`'s competence. The mind was learning
|
|
160
|
+
* it is bad at speaking from the times it decided not to speak. Live, a COO
|
|
161
|
+
* declining correctly ("nothing new to add — a sixth message would repeat")
|
|
162
|
+
* took a competence hit for the judgement.
|
|
163
|
+
*
|
|
164
|
+
* Silence is an ANSWER, not the absence of one. Held here so the sweep can
|
|
165
|
+
* resolve it as what it is: the intent is freed, and nothing is taught about
|
|
166
|
+
* an ability that was never in question.
|
|
167
|
+
*/
|
|
168
|
+
private _withheld = new Map<string, string>()
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The tick at which each held intent's words were REQUESTED from a facet.
|
|
172
|
+
*
|
|
173
|
+
* Authoring is off-tick and can take 10–30s of real time. In that window the
|
|
174
|
+
* situation the words were composed for can move — and until now nothing
|
|
175
|
+
* looked. See `situationMoved`.
|
|
176
|
+
*/
|
|
177
|
+
private _composedAt = new Map<string, Tick>()
|
|
178
|
+
|
|
91
179
|
constructor( schemas: MotorSchema[] = INNATE_SCHEMAS ){
|
|
92
180
|
for( const s of schemas ) this._schemas.set( s.id, s )
|
|
93
181
|
}
|
|
@@ -115,6 +203,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
115
203
|
{ type: 'agency.enacted', version: 1, validate: () => null },
|
|
116
204
|
{ type: 'agency.invocation', version: 1, validate: () => null },
|
|
117
205
|
{ type: 'agency.communicate', version: 1, validate: () => null },
|
|
206
|
+
{ type: 'action.withheld', version: 1, validate: () => null },
|
|
118
207
|
]
|
|
119
208
|
}
|
|
120
209
|
subscribes(): string[] { return [] }
|
|
@@ -123,7 +212,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
123
212
|
* The executor is plan-agnostic. A plan does NOT dispatch steps here — it biases
|
|
124
213
|
* the affordance competition (see PLANNING_AS_PRIOR_TODO.md), so a plan-driven
|
|
125
214
|
* action reaches the executor as an ordinary committed `agency.intent` the
|
|
126
|
-
* selector won. That intent already carries planId/stepId
|
|
215
|
+
* selector won. That intent already carries its planId/stepId link (stamped by
|
|
127
216
|
* the selector from the winning affordance); `_emitActionOutcome` threads it back
|
|
128
217
|
* out, which is how the PlanningEngine advances. Nothing plan-specific here.
|
|
129
218
|
*/
|
|
@@ -140,6 +229,10 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
140
229
|
const set: EntityInput[] = []
|
|
141
230
|
const del: string[] = []
|
|
142
231
|
const metrics: Array<[ string, number ]> = []
|
|
232
|
+
// Dispatch announcements bound for the SIMULATION bus — the only one the host
|
|
233
|
+
// hears. See _emitDispatch: the cognitive copy reaches the mind's own
|
|
234
|
+
// faculties and reaches no host at all.
|
|
235
|
+
const events: Array<Omit<SimulationEvent, 'id' | 'timestamp' | 'tick'>> = []
|
|
143
236
|
|
|
144
237
|
const energy = state.metrics.get('energy.level') ?? 100
|
|
145
238
|
const stress = state.metrics.get('stress.load' ) ?? 0
|
|
@@ -152,6 +245,22 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
152
245
|
if( tick >= num( e.metadata?.['expiresAt'], 0 ) ) del.push( id )
|
|
153
246
|
}
|
|
154
247
|
|
|
248
|
+
// ── Expire durable enaction records ──────────────────────────
|
|
249
|
+
// One per (schema, target), refreshed in place, so the live set is bounded
|
|
250
|
+
// by pairs actually enacted rather than by enactions — but a Will that meets
|
|
251
|
+
// many people would still accrete one per person per schema forever, and the
|
|
252
|
+
// soak test asserts entities plateau. Dropped once no satiation window could
|
|
253
|
+
// still be reading them: past ENACTED_RETENTION_TICKS the term is 0 anyway,
|
|
254
|
+
// so the record is indistinguishable from its own absence.
|
|
255
|
+
for( const [ id, e ] of state.entities ){
|
|
256
|
+
if( e.type !== ENACTED_TYPE ) continue
|
|
257
|
+
const at = num( e.metadata?.['tick'], 0 )
|
|
258
|
+
// A record stamped later than now is a restored one from a previous
|
|
259
|
+
// session (the tick counter restarts on wake) — same trap `liveConsequences`
|
|
260
|
+
// documents. Drop it rather than let it read as "just now" all session.
|
|
261
|
+
if( at > tick || tick - at > ENACTED_RETENTION_TICKS ) del.push( id )
|
|
262
|
+
}
|
|
263
|
+
|
|
155
264
|
// ── Revocation tombstones (EXAFFERENCE P4) ───────────────────
|
|
156
265
|
// Reap expired tombstones (intent vanished otherwise), and collect live ones
|
|
157
266
|
// so the selected-processing loop can refuse the half-race case: Deliberation
|
|
@@ -173,18 +282,122 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
173
282
|
else if( parentId && revoked.has( parentId ) ) del.push( id )
|
|
174
283
|
}
|
|
175
284
|
|
|
285
|
+
// ── Words that landed off-tick → deliver now ─────────────────
|
|
286
|
+
// Second half of two-phase authoring: a previous tick asked a facet for the words
|
|
287
|
+
// and held the intent 'awaiting'; the facet answered between ticks. Runs BEFORE
|
|
288
|
+
// the timeout sweep so words arriving on the same tick the clock would expire are
|
|
289
|
+
// still spoken rather than discarded.
|
|
290
|
+
const spokeThisTick = new Set<string>()
|
|
291
|
+
for( const [ id, e ] of state.entities ){
|
|
292
|
+
if( e.type !== 'agency.intent' || str( e.metadata?.['status'] ) !== 'awaiting') continue
|
|
293
|
+
if( !this._authored.has( id ) ) continue
|
|
294
|
+
|
|
295
|
+
const intent = readIntent( id, e.metadata )
|
|
296
|
+
const predicted: EfferenceCopy = {
|
|
297
|
+
expectedReward: num( e.metadata?.['predictedReward'], intent.expectedReward ),
|
|
298
|
+
expectedValence: num( e.metadata?.['predictedValence'], intent.expectedValence ),
|
|
299
|
+
}
|
|
300
|
+
if( await this._deliver( id, intent, predicted, state, tick, set, del, metrics ) )
|
|
301
|
+
spokeThisTick.add( id )
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Words whose intent no longer exists (revoked, timed out, superseded) are words
|
|
305
|
+
// that will never be said — drop them so the map cannot grow without bound.
|
|
306
|
+
for( const id of this._authored.keys() )
|
|
307
|
+
if( !state.entities.has( id ) ) this._authored.delete( id )
|
|
308
|
+
for( const id of this._withheld.keys() )
|
|
309
|
+
if( !state.entities.has( id ) ) this._withheld.delete( id )
|
|
310
|
+
for( const id of this._composedAt.keys() )
|
|
311
|
+
if( !state.entities.has( id ) ) this._composedAt.delete( id )
|
|
312
|
+
|
|
176
313
|
// ── Timeout stranded async intents ───────────────────────────
|
|
177
314
|
// An 'awaiting' intent whose host/delivery never returned would block the
|
|
178
315
|
// serial Will forever. After AWAIT_TIMEOUT ticks, abandon it as a failed
|
|
179
316
|
// outcome (which also teaches reafference the action is unreliable here).
|
|
180
317
|
for( const [ id, e ] of state.entities ){
|
|
181
318
|
if( e.type !== 'agency.intent' || str( e.metadata?.['status'] ) !== 'awaiting') continue
|
|
319
|
+
// Already delivered above — its outcome and deletion are queued for this tick.
|
|
320
|
+
if( spokeThisTick.has( id ) ) continue
|
|
321
|
+
// Words are being authored right now. The clock PAUSES rather than extends: a
|
|
322
|
+
// facet LLM call is 10–30s of latency we do not control, and any fixed budget
|
|
323
|
+
// large enough to cover a slow one is also large enough to strand a dead one.
|
|
324
|
+
// While a call is genuinely in flight the world has not failed to answer. The
|
|
325
|
+
// pause is bounded, not open-ended: `authorOutreach` always settles (its own 60s
|
|
326
|
+
// timeout resolves empty), so `_authoring` always clears and the clock resumes.
|
|
327
|
+
if( this._authoring.has( id ) ) continue
|
|
328
|
+
// The facet was asked and chose silence. Resolve it as that rather than
|
|
329
|
+
// letting the clock abandon it as a failure — see `_withheld`.
|
|
330
|
+
if( this._withheld.has( id ) ){
|
|
331
|
+
const intent = readIntent( id, e.metadata )
|
|
332
|
+
const why = this._withheld.get( id ) ?? 'I considered speaking and chose not to.'
|
|
333
|
+
set.push({
|
|
334
|
+
id: `agency-outcome-${ tick }-${ id }`,
|
|
335
|
+
type: 'agency.outcome',
|
|
336
|
+
metadata: {
|
|
337
|
+
schema: intent.schema, intentId: id,
|
|
338
|
+
targetEntityId: intent.targetEntityId,
|
|
339
|
+
withheld: true,
|
|
340
|
+
description: why,
|
|
341
|
+
mode: 'communicate', tick,
|
|
342
|
+
...( intent.planId ? { planId: intent.planId } : {} ),
|
|
343
|
+
...( intent.planStepId ? { stepId: intent.planStepId } : {} ),
|
|
344
|
+
},
|
|
345
|
+
})
|
|
346
|
+
del.push( id )
|
|
347
|
+
this._withheld.delete( id )
|
|
348
|
+
metrics.push([ 'agency.communicate.withheld', 1 ])
|
|
349
|
+
// A DISTINCT event, never `action.outcome`: six faculties learn from that
|
|
350
|
+
// one's `success`, and a withheld turn published as `success: false`
|
|
351
|
+
// teaches the mind it is bad at speaking from the times it decided not to
|
|
352
|
+
// speak — the exact regression #123 exists to prevent. This one is read
|
|
353
|
+
// only by the executive's record of what it did.
|
|
354
|
+
if( this._bus ){
|
|
355
|
+
try {
|
|
356
|
+
this._bus.publish({
|
|
357
|
+
type: 'action.withheld', version: 1, sourceEngine: this.name,
|
|
358
|
+
salience: 0.4,
|
|
359
|
+
payload: {
|
|
360
|
+
actionType: intent.schema, targetEntityId: intent.targetEntityId,
|
|
361
|
+
description: why, tick,
|
|
362
|
+
...( intent.planId ? { planId: intent.planId } : {} ),
|
|
363
|
+
},
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
catch { /* unregistered schema is telemetry-only */ }
|
|
367
|
+
}
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
182
370
|
// POLICY_REAFFERENCE P4 — an escalated intent is HELD: the stem owns its
|
|
183
371
|
// lifecycle (extended TTL → approve/deny/expire), so the executor must not
|
|
184
372
|
// time it out at AWAIT_TIMEOUT and reconcile it as a phantom failure.
|
|
185
373
|
if( e.metadata?.['escalated'] === true ) continue
|
|
186
374
|
const dispatchedAt = num( e.metadata?.['dispatchedAt'], tick )
|
|
187
|
-
|
|
375
|
+
const age = tick - dispatchedAt
|
|
376
|
+
|
|
377
|
+
// Dispatched LATER than now ⇒ it was in flight when the mind went to sleep.
|
|
378
|
+
// Intents snapshot with the state and the tick counter restarts at 1 on wake,
|
|
379
|
+
// so a restored `awaiting` intent has a NEGATIVE age — and every guard here
|
|
380
|
+
// inverts on it: `age < AWAIT_TIMEOUT` is trivially true for -589, so it never
|
|
381
|
+
// timed out, and the selector's staleness went negative, INFLATING the
|
|
382
|
+
// incumbent's strength instead of decaying it. Measured on a live Will:
|
|
383
|
+
// incumbent 9.742 against challengers of 0.52, unpreemptable and immortal.
|
|
384
|
+
// One person's stale intent held the channel and every attempt to contact
|
|
385
|
+
// anyone else was refused, indefinitely, across restarts.
|
|
386
|
+
//
|
|
387
|
+
// Cleared WITHOUT a failure outcome: hibernating is not the world declining
|
|
388
|
+
// to answer. Reconciling it as a timeout would teach reafference that
|
|
389
|
+
// reaching that person does not work, which is a lesson about the process
|
|
390
|
+
// lifecycle, not about them.
|
|
391
|
+
if( age < 0 ){
|
|
392
|
+
del.push( id )
|
|
393
|
+
logger.info(
|
|
394
|
+
`[motor] cleared "${ str( e.metadata?.['schema'] ) ?? 'intent' }" left awaiting ` +
|
|
395
|
+
`across a restart (dispatched at tick ${ dispatchedAt }, now ${ tick })`
|
|
396
|
+
)
|
|
397
|
+
continue
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if( age < AWAIT_TIMEOUT ) continue
|
|
188
401
|
|
|
189
402
|
const intent = readIntent( id, e.metadata )
|
|
190
403
|
const predicted: EfferenceCopy = {
|
|
@@ -199,7 +412,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
199
412
|
del.push( id )
|
|
200
413
|
this._emitEnacted( intent, timedOut, predicted, tick )
|
|
201
414
|
if( intent.planId && intent.planStepId )
|
|
202
|
-
this._emitActionOutcome( intent, false, 0, 1, tick
|
|
415
|
+
this._emitActionOutcome( intent, false, 0, 1, tick,
|
|
416
|
+
`No answer came back — I gave up waiting after ${ tick - dispatchedAt } ticks.` )
|
|
203
417
|
logger.info(`[motor] ⏱ "${ intent.schema }" timed out after ${ tick - dispatchedAt } ticks`)
|
|
204
418
|
}
|
|
205
419
|
|
|
@@ -242,7 +456,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
242
456
|
enactedCount++
|
|
243
457
|
this._emitEnacted( intent, enaction, predicted, tick )
|
|
244
458
|
this._emitActionOutcome( intent, enaction.success, enaction.outcomeQuality,
|
|
245
|
-
clamp01( Math.abs( predicted.expectedReward - enaction.outcomeQuality ) ), tick
|
|
459
|
+
clamp01( Math.abs( predicted.expectedReward - enaction.outcomeQuality ) ), tick,
|
|
460
|
+
enaction.description )
|
|
246
461
|
|
|
247
462
|
// If this was a macro step, advance (or finalize) its parent.
|
|
248
463
|
if( intent.parentIntentId )
|
|
@@ -281,6 +496,13 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
281
496
|
set.push( consequenceEntity({
|
|
282
497
|
intentId: id, schema: intent.schema,
|
|
283
498
|
mode: enaction.mode === 'communicate' ? 'communicate' : 'external',
|
|
499
|
+
// A communicate with no words yet has not happened. The footprint is
|
|
500
|
+
// still written (P1/P2 want it the moment the words land), but it must
|
|
501
|
+
// not satiate — attempting to speak is not speaking. See
|
|
502
|
+
// ConsequenceDescriptor.pending. An EXTERNAL dispatch IS the act: the
|
|
503
|
+
// host is doing it now, and not asking twice while waiting is exactly
|
|
504
|
+
// what satiation is for.
|
|
505
|
+
...( enaction.mode === 'communicate' && !awaitingText ? { pending: true } : {} ),
|
|
284
506
|
...( enaction.mode === 'communicate'
|
|
285
507
|
? { effector: COMM_SCHEMA_TO_EFFECTOR[ intent.schema ] ?? intent.schema }
|
|
286
508
|
: {} ),
|
|
@@ -289,7 +511,16 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
289
511
|
paramsHash: fnv1a( paramsKey( intent.parameters ) ),
|
|
290
512
|
expiresAt: tick + CONSEQUENCE_TTL_TICKS, tick,
|
|
291
513
|
}) )
|
|
292
|
-
|
|
514
|
+
// The durable half, for an act with an object that is not speech.
|
|
515
|
+
// Speech has `conversation.sent` and objectless acts have
|
|
516
|
+
// `LearnedSkill.lastEnactedTick`; this is the peer those two left out,
|
|
517
|
+
// and without it satiation expired with the descriptor at the ECHO
|
|
518
|
+
// window rather than lasting the satiation window. Written at the same
|
|
519
|
+
// moment as the descriptor — the dispatch IS the act for an external
|
|
520
|
+
// effector — and keyed per (schema, target) so it refreshes in place.
|
|
521
|
+
if( intent.targetEntityId && enaction.mode !== 'communicate')
|
|
522
|
+
set.push( enactedEntity( intent.schema, intent.targetEntityId, tick ) )
|
|
523
|
+
events.push( ...this._emitDispatch( intent, enaction.mode, tick, state ) )
|
|
293
524
|
metrics.push([ enaction.mode === 'communicate'
|
|
294
525
|
? 'agency.communicate.dispatched'
|
|
295
526
|
: 'agency.invocation.dispatched', 1 ])
|
|
@@ -298,7 +529,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
298
529
|
}
|
|
299
530
|
|
|
300
531
|
metrics.push([ 'agency.executor.enacted', enactedCount ])
|
|
301
|
-
return { commands: { set, delete: del, metrics } }
|
|
532
|
+
return { commands: { set, delete: del, metrics }, ...( events.length > 0 ? { events } : {} ) }
|
|
302
533
|
}
|
|
303
534
|
|
|
304
535
|
// ── composite machinery ──────────────────────────────────────
|
|
@@ -374,7 +605,8 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
374
605
|
this._emitEnacted( compIntent, compEnaction,
|
|
375
606
|
{ expectedReward: compIntent.expectedReward, expectedValence: compIntent.expectedValence }, tick )
|
|
376
607
|
this._emitActionOutcome( compIntent, true, avgQuality,
|
|
377
|
-
clamp01( Math.abs( compIntent.expectedReward - avgQuality ) ), tick
|
|
608
|
+
clamp01( Math.abs( compIntent.expectedReward - avgQuality ) ), tick,
|
|
609
|
+
compEnaction.description )
|
|
378
610
|
}
|
|
379
611
|
|
|
380
612
|
private _subIntent(
|
|
@@ -437,17 +669,62 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
437
669
|
return true
|
|
438
670
|
}
|
|
439
671
|
|
|
440
|
-
//
|
|
441
|
-
// self-initiated
|
|
442
|
-
//
|
|
672
|
+
// ── Am I mid-sentence with them? ─────────────────────────────
|
|
673
|
+
// A reply and a self-initiated message are two paths to the same person, and
|
|
674
|
+
// neither can see the other. Observed live: a conversation facet answered a
|
|
675
|
+
// question and the agency delivered a proactive message to the same human in
|
|
676
|
+
// the SAME millisecond — one mind speaking to one person twice at once.
|
|
677
|
+
//
|
|
678
|
+
// Holding costs a tick and the words keep. If what they just said changes
|
|
679
|
+
// what I meant to say, `situationMoved` catches that on the way back round,
|
|
680
|
+
// which is the outcome we want anyway.
|
|
681
|
+
if( intent.targetEntityId && this._author?.isSpeakingTo?.( intent.targetEntityId ) ){
|
|
682
|
+
logger.debug(`[motor] holding "${ intent.schema }" — a turn with ${ intent.targetEntityId } is in flight`)
|
|
683
|
+
metrics.push([ 'agency.communicate.mid_turn', 1 ])
|
|
684
|
+
return false
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Content authored upstream when present (host / host-facet), else words a facet
|
|
688
|
+
// authored off-tick and landed since a previous tick asked for them. Neither ⇒
|
|
689
|
+
// request authoring (never awaited here — see `_authoring`) and hold 'awaiting'.
|
|
443
690
|
const authored = str( intent.parameters['content'] ) ?? firstMessage( intent.parameters['messages'] )
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
691
|
+
const fromFacet = !authored
|
|
692
|
+
let bubbles: string[] = authored ? [ authored ] : ( this._authored.get( id ) ?? [] )
|
|
693
|
+
this._authored.delete( id )
|
|
694
|
+
if( bubbles.length === 0 ){
|
|
695
|
+
this._requestAuthoring( id, intent, tick )
|
|
696
|
+
return false // nothing to send yet → await
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ── Is this still what I want to say? ─────────────────────────
|
|
700
|
+
// Words a facet composed off-tick were formed against the situation as it
|
|
701
|
+
// stood when they were asked for. Delivering them into a situation that has
|
|
702
|
+
// since moved is not the act that was decided on — it is an older act
|
|
703
|
+
// arriving late wearing the present's clothes.
|
|
704
|
+
//
|
|
705
|
+
// Live: a COO committed to outreach, was told "I need my full attention on
|
|
706
|
+
// that right now, will brief you later", acknowledged that correctly — and
|
|
707
|
+
// 41 seconds later delivered the three-bubble message composed beforehand,
|
|
708
|
+
// asking for a brain-dump. Same shape at 3am with a colleague who had just
|
|
709
|
+
// said goodnight: "Night. Talk soon." followed one second later by two
|
|
710
|
+
// unprompted messages. Satiation cannot damp either, because satiation gates
|
|
711
|
+
// SELECTION and these were already selected. Nothing looked at the door.
|
|
712
|
+
//
|
|
713
|
+
// Only facet-composed words are checked. Content supplied upstream
|
|
714
|
+
// (`parameters.content`) came from the host or a host-facet with the present
|
|
715
|
+
// situation in hand, and a reply never reaches this path at all — it is
|
|
716
|
+
// delivered by the audition facet through the outbox.
|
|
717
|
+
const moved = fromFacet
|
|
718
|
+
? situationMoved( state, intent.targetEntityId, this._composedAt.get( id ) )
|
|
719
|
+
: null
|
|
720
|
+
if( moved ){
|
|
721
|
+
this._withheld.set( id, moved )
|
|
722
|
+
this._composedAt.delete( id )
|
|
723
|
+
logger.debug(`[motor] "${ intent.schema }" withheld — ${ moved }`)
|
|
724
|
+
metrics.push([ 'agency.communicate.stale', 1 ])
|
|
725
|
+
return false // resolved as withheld by the sweep: freed, and nothing taught
|
|
449
726
|
}
|
|
450
|
-
|
|
727
|
+
this._composedAt.delete( id )
|
|
451
728
|
|
|
452
729
|
const request: ActionRequest = {
|
|
453
730
|
effector,
|
|
@@ -477,13 +754,75 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
477
754
|
text: bubbles.join('\n'),
|
|
478
755
|
expiresAt: tick + CONSEQUENCE_TTL_TICKS, tick,
|
|
479
756
|
}) )
|
|
757
|
+
// The words are out. Whatever willed them is DONE being an intention.
|
|
758
|
+
//
|
|
759
|
+
// Discharged here rather than at dispatch on purpose: `_deliver` holds the
|
|
760
|
+
// intent 'awaiting' while a facet authors, and that authoring can time out or
|
|
761
|
+
// return nothing. Clearing the will at dispatch would lose the intention
|
|
762
|
+
// silently — the mind would have decided to say something and simply never
|
|
763
|
+
// have, which is the failure `commands.ts` calls "the whole intention
|
|
764
|
+
// evaporated without a trace". Enaction is the discharge; a request is not.
|
|
765
|
+
if( result.success ) this._dischargeWill( intent, del )
|
|
766
|
+
|
|
480
767
|
this._emitEnacted( intent, out, predicted, tick )
|
|
481
768
|
this._emitActionOutcome( intent, result.success, result.feedback.outcomeQuality,
|
|
482
|
-
clamp01( Math.abs( predicted.expectedReward - result.feedback.outcomeQuality ) ), tick
|
|
769
|
+
clamp01( Math.abs( predicted.expectedReward - result.feedback.outcomeQuality ) ), tick,
|
|
770
|
+
out.description )
|
|
483
771
|
metrics.push([ 'agency.communicate.delivered', 1 ])
|
|
484
772
|
return true
|
|
485
773
|
}
|
|
486
774
|
|
|
775
|
+
/**
|
|
776
|
+
* Ask a facet for the words, off-tick. Fire-and-forget on purpose: awaiting this
|
|
777
|
+
* from inside `react()` deadlocks the tick loop against the facet pump. Idempotent
|
|
778
|
+
* per intent — a request already in flight is not duplicated, so the intent may sit
|
|
779
|
+
* 'awaiting' across many ticks with exactly one LLM call behind it.
|
|
780
|
+
*/
|
|
781
|
+
private _requestAuthoring( id: string, intent: Intent, tick: Tick ): void {
|
|
782
|
+
if( !this._author || this._authoring.has( id ) ) return
|
|
783
|
+
|
|
784
|
+
const name = str( intent.parameters['targetEntityName'] ) ?? intent.targetEntityId ?? 'them'
|
|
785
|
+
this._authoring.add( id )
|
|
786
|
+
this._composedAt.set( id, tick )
|
|
787
|
+
void this._author
|
|
788
|
+
.authorOutreach( intent.targetEntityId ?? '', name, str( intent.parameters['gist'] ) )
|
|
789
|
+
.then( result => {
|
|
790
|
+
const bubbles = Array.isArray( result ) ? result : result.bubbles
|
|
791
|
+
const declared = !Array.isArray( result ) && result.withheld === true
|
|
792
|
+
const answered = !Array.isArray( result ) && result.answered === true
|
|
793
|
+
|
|
794
|
+
if( bubbles.length > 0 ){ this._authored.set( id, bubbles ); return }
|
|
795
|
+
|
|
796
|
+
// Not a warning: the facet was asked and answered. Choosing not to speak
|
|
797
|
+
// is a decision the mind is entitled to make.
|
|
798
|
+
if( declared ){
|
|
799
|
+
this._withheld.set( id, 'I considered speaking and chose not to.')
|
|
800
|
+
logger.debug(`[motor] "${ intent.schema }" withheld — the facet chose silence`)
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
// Asked, reasoned, and came back with nothing — neither words nor a
|
|
804
|
+
// declared silence. That is still an answer, and re-asking puts the same
|
|
805
|
+
// question to the same mind against the same situation, so it comes back
|
|
806
|
+
// the same way. Nineteen times, in the run this exists to prevent.
|
|
807
|
+
//
|
|
808
|
+
// Resolved as withheld rather than failed on purpose: nothing was said,
|
|
809
|
+
// and nothing should be taught about being bad at speaking — the #123
|
|
810
|
+
// regression. The reason is worded for what actually happened.
|
|
811
|
+
if( answered ){
|
|
812
|
+
this._withheld.set( id, 'I turned it over and came back with no words.')
|
|
813
|
+
logger.info(`[motor] "${ intent.schema }" withheld — the pass produced no words`)
|
|
814
|
+
return
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// No pass ran at all — a dead author, a full budget, or one deferring to
|
|
818
|
+
// a pass already in flight. Keep holding: the clock is the right judge of
|
|
819
|
+
// those, and the deferring case wants to come back round.
|
|
820
|
+
logger.warn(`[motor] outreach authoring never ran for "${ intent.schema }"`)
|
|
821
|
+
} )
|
|
822
|
+
.catch( err => { logger.warn(`[motor] outreach authoring failed: ${ errMsg( err ) }`) } )
|
|
823
|
+
.finally( () => { this._authoring.delete( id ) } )
|
|
824
|
+
}
|
|
825
|
+
|
|
487
826
|
// ── bus emission ─────────────────────────────────────────────
|
|
488
827
|
|
|
489
828
|
private _emitEnacted( intent: Intent, enaction: Enaction, predicted: EfferenceCopy, tick: Tick ): void {
|
|
@@ -506,8 +845,48 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
506
845
|
* RewardEvaluator reads it as a reward signal. `confidence` carries the agency's
|
|
507
846
|
* own forward-model prior so calibration has a real prediction to score.
|
|
508
847
|
*/
|
|
848
|
+
/**
|
|
849
|
+
* Retire the `ideomotor.intent` that produced this act.
|
|
850
|
+
*
|
|
851
|
+
* Nothing deleted these. They were cleared only when the executive next ran and
|
|
852
|
+
* declined to name the same action again — and the executive runs on an interval,
|
|
853
|
+
* so between cycles a willed reach-out STOOD in state, was rebuilt into an
|
|
854
|
+
* affordance every single tick, and competed every single tick. Observed as
|
|
855
|
+
* dozens of identical lines:
|
|
856
|
+
*
|
|
857
|
+
* [selector] willed reach-out → … NOT selected: 0.297 < inspect… 0.340
|
|
858
|
+
*
|
|
859
|
+
* losing by four thousandths, over and over, until it won — twice. Fabrice got
|
|
860
|
+
* the same message byte-for-byte 25 ticks apart, two outbox ids.
|
|
861
|
+
*
|
|
862
|
+
* `justEnacted` was built to hold this line and cannot: it is a DECAYING
|
|
863
|
+
* quantity, capped at `repeatDamping` (0.30), and a standing intent outlasts it
|
|
864
|
+
* by construction. Damping a permanent pull only ever delays it. So the intention
|
|
865
|
+
* is discharged by being acted on, which is what an intention is — you meant to
|
|
866
|
+
* tell someone something, you told them, and it is finished. If the mind still
|
|
867
|
+
* wants to say more, the next executive cycle forms a new one, now seeing "I said
|
|
868
|
+
* this 25 ticks ago and have had no answer" in front of it.
|
|
869
|
+
*
|
|
870
|
+
* Satiation stays exactly as it was, and still earns its keep: it damps saying
|
|
871
|
+
* the same thing again for reasons that did NOT come from a standing will.
|
|
872
|
+
*/
|
|
873
|
+
private _dischargeWill( intent: Intent, del: string[] ): void {
|
|
874
|
+
const willId = intent.evokedBy
|
|
875
|
+
if( !willId || !willId.startsWith('ideomotor-') ) return
|
|
876
|
+
del.push( willId )
|
|
877
|
+
logger.info(`[motor] discharged the will behind "${ intent.schema }" (${ willId })`)
|
|
878
|
+
}
|
|
879
|
+
|
|
509
880
|
private _emitActionOutcome(
|
|
510
881
|
intent: Intent, success: boolean, outcomeQuality: number, surprise: number, tick: Tick,
|
|
882
|
+
/**
|
|
883
|
+
* What happened, in words. This payload builds `action.record`, which the
|
|
884
|
+
* prompt renders as `## What Became Of What I Did` — and until now this method
|
|
885
|
+
* published no description at all, so that section showed the action's NAME
|
|
886
|
+
* and nothing else. Sixty-five lookups rendered as sixty-five lines saying
|
|
887
|
+
* `discord_lookup_member` and never once what was found.
|
|
888
|
+
*/
|
|
889
|
+
description?: string,
|
|
511
890
|
): void {
|
|
512
891
|
if( !this._bus ) return
|
|
513
892
|
try {
|
|
@@ -518,6 +897,7 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
518
897
|
actionType: intent.schema,
|
|
519
898
|
domain: intent.schema,
|
|
520
899
|
confidence: intent.expectedReward,
|
|
900
|
+
...( description ? { description } : {} ),
|
|
521
901
|
success,
|
|
522
902
|
outcomeQuality,
|
|
523
903
|
surprise,
|
|
@@ -531,21 +911,67 @@ export class MotorSchemaExecutor implements CognitiveEngine {
|
|
|
531
911
|
catch( err ){ logger.warn(`[motor] action outcome publish failed: ${ errMsg( err ) }`) }
|
|
532
912
|
}
|
|
533
913
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
914
|
+
/**
|
|
915
|
+
* Announce a dispatched act to its TWO audiences, which live on two buses.
|
|
916
|
+
*
|
|
917
|
+
* The mind's own faculties appraise it — affective.blender, stress.regulator
|
|
918
|
+
* and attention.allocator all subscribe to `agency.invocation` on the
|
|
919
|
+
* CognitiveBus, the internal fabric. But the act is host-owned, and the HOST
|
|
920
|
+
* hears only `simulation.eventBus`: `WillStem` buffers an invocation for
|
|
921
|
+
* delivery from `eventBus.subscribeAll`, and that is the sole path by which an
|
|
922
|
+
* external effector ever reaches a handler.
|
|
923
|
+
*
|
|
924
|
+
* Only the cognitive half was ever published. The two buses have no bridge, so
|
|
925
|
+
* `bufferInvocation` was subscribed to a bus that has never carried the event
|
|
926
|
+
* — measured on a live boot: `agency.invocation.dispatched` incremented,
|
|
927
|
+
* `pendingEffectorInvocations` stayed empty, the intent held `awaiting`, and
|
|
928
|
+
* fifteen ticks later `[motor] ⏱ "inspect" timed out`. **No host-owned
|
|
929
|
+
* effector invocation has ever been delivered.** Communication is unaffected —
|
|
930
|
+
* the outbox is a separate mechanism, which is why a Will could always speak.
|
|
931
|
+
*
|
|
932
|
+
* Both ends were unit-tested and the crossing was not: `policy.*.test.ts` calls
|
|
933
|
+
* `bufferInvocation` directly, which is true about the controller and silent
|
|
934
|
+
* about whether anything reaches it. Same shape as the affordance-field hop.
|
|
935
|
+
*/
|
|
936
|
+
private _emitDispatch(
|
|
937
|
+
intent: Intent, mode: 'communicate' | 'external', tick: Tick,
|
|
938
|
+
state?: ReadonlySimulationState,
|
|
939
|
+
): Array<Omit<SimulationEvent, 'id' | 'timestamp' | 'tick'>> {
|
|
940
|
+
const payload = this._dispatchPayload( intent, tick, state )
|
|
941
|
+
const type = mode === 'communicate' ? 'agency.communicate' : 'agency.invocation'
|
|
942
|
+
|
|
943
|
+
if( this._bus )
|
|
944
|
+
try {
|
|
945
|
+
this._bus.publish({ type, version: 1, sourceEngine: this.name, salience: 0.6, payload })
|
|
946
|
+
}
|
|
947
|
+
catch( err ){ logger.warn(`[motor] dispatch publish failed: ${ errMsg( err ) }`) }
|
|
948
|
+
|
|
949
|
+
// The observable bus — where the host is listening.
|
|
950
|
+
return [ { type, source: this.name, payload } ]
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private _dispatchPayload(
|
|
954
|
+
intent: Intent, tick: Tick, state?: ReadonlySimulationState,
|
|
955
|
+
): Record<string, unknown> {
|
|
956
|
+
return {
|
|
541
957
|
schema: intent.schema, intentId: intent.id,
|
|
542
958
|
targetEntityId: intent.targetEntityId, parameters: intent.parameters, tick,
|
|
543
959
|
// The ability's declared meaning, carried to the host handler.
|
|
544
960
|
description: this._resolve( intent.schema )?.description,
|
|
545
|
-
|
|
546
|
-
|
|
961
|
+
// Where in the world this referent is, so a host can act on it at all.
|
|
962
|
+
//
|
|
963
|
+
// The target is an ANCHOR (`ke:1sqlkux`) — 0.9.0 made identity opaque and
|
|
964
|
+
// separate from address on purpose. A bridge holds channel ids and knows
|
|
965
|
+
// nothing of anchors, so an invocation naming only the referent is one no
|
|
966
|
+
// surface can serve. The outbox hit this first and solved it the same way
|
|
967
|
+
// (mind.ts attachRouting): resolve inside, where the alias table lives.
|
|
968
|
+
//
|
|
969
|
+
// Outbound only. This is the mind telling the world which of its own
|
|
970
|
+
// handles it means — the reverse direction, a surface writing an address
|
|
971
|
+
// into the mind, is what perception is for.
|
|
972
|
+
...( state && intent.targetEntityId
|
|
973
|
+
? { targetAddresses: addressesOf( state.entities, intent.targetEntityId ) } : {} ),
|
|
547
974
|
}
|
|
548
|
-
catch( err ){ logger.warn(`[motor] dispatch publish failed: ${ errMsg( err ) }`) }
|
|
549
975
|
}
|
|
550
976
|
}
|
|
551
977
|
|
|
@@ -588,6 +1014,7 @@ function readIntent( id: string, m: ReadonlyMap<string, unknown> | Record<string
|
|
|
588
1014
|
stepIndex: typeof meta['stepIndex'] === 'number' ? ( meta['stepIndex'] as number ) : undefined,
|
|
589
1015
|
planId: str( meta['planId'] ),
|
|
590
1016
|
planStepId: str( meta['stepId'] ),
|
|
1017
|
+
evokedBy: str( meta['evokedBy'] ),
|
|
591
1018
|
}
|
|
592
1019
|
}
|
|
593
1020
|
|
|
@@ -608,3 +1035,53 @@ function clamp01( n: number ): number {
|
|
|
608
1035
|
function errMsg( err: unknown ): string {
|
|
609
1036
|
return err instanceof Error ? err.message : String( err )
|
|
610
1037
|
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Has the situation these words were composed for moved on?
|
|
1041
|
+
*
|
|
1042
|
+
* Returns the mind's own account of what changed, or null if the moment still
|
|
1043
|
+
* stands. The account is what the withheld outcome records, so the mind's
|
|
1044
|
+
* history says why it did not speak rather than merely that it didn't.
|
|
1045
|
+
*
|
|
1046
|
+
* The evidence is `conversation.sent` — the durable record of having spoken,
|
|
1047
|
+
* written by every path that speaks (ProactiveCommunicator, the audition facet,
|
|
1048
|
+
* the outbox). If I have said something to this person SINCE these words were
|
|
1049
|
+
* asked for, then these are not my current words to them: I have already
|
|
1050
|
+
* responded to whatever moved in between, and this is an older turn arriving on
|
|
1051
|
+
* top of a newer one.
|
|
1052
|
+
*
|
|
1053
|
+
* And `answeredAt` — when they last spoke to ME. This arm was missing at first,
|
|
1054
|
+
* excluded on the argument that `conversation.received` is a one-shot entity
|
|
1055
|
+
* swept on the tick it is scanned so nothing durable records the other
|
|
1056
|
+
* direction. That was wrong: `answeredAt` is the durable half, folded onto the
|
|
1057
|
+
* mind's own sent turns, and it is what renders "they answered" in the prompt.
|
|
1058
|
+
*
|
|
1059
|
+
* The gap was the COMMON case, not an edge. Live, a COO delivered a pre-composed
|
|
1060
|
+
* agenda message ("I need a list of what's actively in flight…") two seconds
|
|
1061
|
+
* after the person changed the subject, having said nothing in between — so the
|
|
1062
|
+
* stale words arrived BEFORE her real reply and the spoken-since arm had not
|
|
1063
|
+
* fired yet. The two signals were assumed to coincide and do not: whoever moved
|
|
1064
|
+
* the situation, it moved.
|
|
1065
|
+
*
|
|
1066
|
+
* A missing `composedAt` means the words were never requested through
|
|
1067
|
+
* `_requestAuthoring` — nothing is known about when they were formed, so nothing
|
|
1068
|
+
* is claimed and they go out. Silence beats a guess here: withholding on absent
|
|
1069
|
+
* evidence would mute a mind for reasons it could not name.
|
|
1070
|
+
*/
|
|
1071
|
+
export function situationMoved(
|
|
1072
|
+
state: ReadonlySimulationState,
|
|
1073
|
+
targetEntityId: string | undefined,
|
|
1074
|
+
composedAt: Tick | undefined,
|
|
1075
|
+
): string | null {
|
|
1076
|
+
if( composedAt === undefined || !targetEntityId ) return null
|
|
1077
|
+
|
|
1078
|
+
const heard = lastAnsweredByEntity( state.entities as never ).get( targetEntityId )
|
|
1079
|
+
if( heard !== undefined && heard > composedAt )
|
|
1080
|
+
return 'They said something after I composed this, so it is an answer to a moment that has passed.'
|
|
1081
|
+
|
|
1082
|
+
const spoke = spokenAtByEntity( state.entities as never ).get( targetEntityId )
|
|
1083
|
+
if( spoke !== undefined && spoke > composedAt )
|
|
1084
|
+
return 'I had already spoken to them since composing this, so it was no longer what I had to say.'
|
|
1085
|
+
|
|
1086
|
+
return null
|
|
1087
|
+
}
|