@mindot/will 0.5.0 → 0.7.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/dist/channels/discord.d.ts +1 -1
- package/dist/channels/discord.js.map +1 -1
- package/dist/channels/whatsapp.d.ts +1 -1
- package/dist/channels/whatsapp.js.map +1 -1
- package/dist/cli.js +998 -59
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +998 -59
- package/dist/index.js.map +1 -1
- package/dist/mcp/effectors.d.ts +1 -1
- package/dist/mcp/effectors.js.map +1 -1
- package/dist/{will-BDq-TMQr.d.ts → will-DAW0l-lY.d.ts} +90 -10
- package/package.json +1 -1
- package/src/channels/discord.ts +11 -11
- package/src/channels/roster.ts +1 -1
- package/src/channels/types.ts +1 -1
- package/src/channels/whatsapp.ts +25 -25
- package/src/cli.ts +20 -20
- package/src/cognition/acp.ts +28 -0
- package/src/cognition/agency/consequence.ts +174 -0
- package/src/cognition/agency/engines/action.selector.ts +333 -48
- package/src/cognition/agency/engines/affordance.synthesizer.ts +35 -16
- package/src/cognition/agency/engines/deliberation.engine.ts +28 -14
- package/src/cognition/agency/engines/motor.schema.executor.ts +90 -14
- package/src/cognition/agency/engines/reafference.engine.ts +152 -23
- package/src/cognition/agency/execution.primitives.ts +13 -13
- package/src/cognition/agency/proactive.communicator.ts +1 -1
- package/src/cognition/agency/reconcile.learning.ts +10 -1
- package/src/cognition/agency/revocation.ts +79 -0
- package/src/cognition/agency/schemas/repertoire.ts +109 -9
- package/src/cognition/agency/selection.scoring.ts +32 -10
- package/src/cognition/agency/types.ts +9 -0
- package/src/cognition/bus.ts +2 -2
- package/src/cognition/completion.inbox.ts +1 -1
- package/src/cognition/config.mirror.entities.ts +5 -5
- package/src/cognition/event.log.ts +2 -2
- package/src/cognition/event.schemas.ts +67 -67
- package/src/cognition/faculties/aesthetic.evaluator.ts +6 -6
- package/src/cognition/faculties/affective.blender.ts +36 -4
- package/src/cognition/faculties/attachment.evaluator.ts +6 -6
- package/src/cognition/faculties/attention.allocator.ts +63 -8
- package/src/cognition/faculties/autobiographical.narrator.ts +3 -3
- package/src/cognition/faculties/bias.detector.ts +7 -7
- package/src/cognition/faculties/circadian.oscillator.ts +3 -3
- package/src/cognition/faculties/confidence.calibrator.ts +7 -7
- package/src/cognition/faculties/dream.simulator.ts +5 -5
- package/src/cognition/faculties/empathy.simulator.ts +7 -7
- package/src/cognition/faculties/energy.regulator.ts +4 -4
- package/src/cognition/faculties/episodic.consolidator.ts +6 -6
- package/src/cognition/faculties/executive.engine/commands.ts +6 -6
- package/src/cognition/faculties/executive.engine/context.ts +42 -42
- package/src/cognition/faculties/executive.engine/deferred.effects.ts +2 -2
- package/src/cognition/faculties/executive.engine/engine.ts +25 -25
- package/src/cognition/faculties/executive.engine/facet.supervisor.ts +12 -12
- package/src/cognition/faculties/executive.engine/facet.ts +18 -18
- package/src/cognition/faculties/executive.engine/gating.ts +2 -2
- package/src/cognition/faculties/executive.engine/messages.ts +3 -3
- package/src/cognition/faculties/executive.engine/parser.ts +11 -11
- package/src/cognition/faculties/executive.engine/prompt.factory.ts +55 -55
- package/src/cognition/faculties/exteroception.ts +99 -5
- package/src/cognition/faculties/forgetting.curve.ts +3 -3
- package/src/cognition/faculties/frustration.evaluator.ts +6 -6
- package/src/cognition/faculties/goal.manager.ts +15 -15
- package/src/cognition/faculties/inhibition.controller.ts +3 -3
- package/src/cognition/faculties/interoception.ts +3 -3
- package/src/cognition/faculties/introspection.engine.ts +3 -3
- package/src/cognition/faculties/known.entity.tracker.ts +15 -15
- package/src/cognition/faculties/loss.evaluator.ts +2 -2
- package/src/cognition/faculties/moral.evaluator.ts +4 -4
- package/src/cognition/faculties/novelty.detector.ts +4 -4
- package/src/cognition/faculties/persona.consolidator.ts +12 -12
- package/src/cognition/faculties/planning.engine/engine.ts +47 -47
- package/src/cognition/faculties/planning.engine/plan.frontier.ts +5 -5
- package/src/cognition/faculties/planning.engine/plan.store.ts +1 -1
- package/src/cognition/faculties/planning.engine/plan.supervision.ts +20 -20
- package/src/cognition/faculties/reputation.tracker.ts +6 -6
- package/src/cognition/faculties/reward.evaluator.ts +4 -4
- package/src/cognition/faculties/self.model.updater.ts +8 -8
- package/src/cognition/faculties/semantic.engine/integrator.ts +15 -15
- package/src/cognition/faculties/sleep.pressure.regulator.ts +4 -4
- package/src/cognition/faculties/social.perception.ts +3 -3
- package/src/cognition/faculties/spaced.repetition.ts +2 -2
- package/src/cognition/faculties/stress.regulator.ts +27 -4
- package/src/cognition/faculties/task.switcher.ts +11 -6
- package/src/cognition/faculties/theory.of.mind.ts +6 -6
- package/src/cognition/faculties/threat.evaluator.ts +3 -3
- package/src/cognition/faculties/working.memory.ts +8 -8
- package/src/cognition/generative.model.ts +2 -2
- package/src/cognition/memory/vector.adapter.ts +5 -5
- package/src/cognition/memory/vector.content.ts +4 -4
- package/src/cognition/memory/vector.embedder.ts +9 -9
- package/src/cognition/persona.prior.ts +1 -1
- package/src/cognition/schema.registry.ts +2 -2
- package/src/cognition/senses/audition.engine/engine.ts +2 -2
- package/src/cognition/senses/base.sense.engine.ts +1 -1
- package/src/cognition/utilities/token.tracker.ts +5 -5
- package/src/core/abstracts.ts +1 -1
- package/src/core/completion.recorder.ts +2 -2
- package/src/core/serialization.ts +3 -3
- package/src/core/snapshot.manager.ts +1 -1
- package/src/core/state.manager.ts +1 -1
- package/src/core/utils.ts +1 -1
- package/src/host/boot.ts +30 -30
- package/src/host/utterances.ts +1 -1
- package/src/llm/index.ts +9 -9
- package/src/llm/summarizer.ts +4 -4
- package/src/llm/wire.contracts.ts +1 -1
- package/src/mcp/effectors.ts +10 -10
- package/src/mcp/server.ts +11 -11
- package/src/pma/eval.ts +1 -1
- package/src/pma/index.ts +21 -21
- package/src/runners/outreach.runner.ts +3 -3
- package/src/runners/social.runner.ts +1 -1
- package/src/runners/thin-shim.runner.ts +1 -1
- package/src/sdk/will.ts +5 -5
- package/src/serve/server.ts +12 -12
- package/src/stem/assembly.audit.ts +3 -3
- package/src/stem/guards/identity.coherence.ts +10 -10
- package/src/stem/guards/identity.guard.ts +22 -22
- package/src/stem/index.ts +26 -11
- package/src/stem/mind.ts +12 -12
- package/src/stem/policy/arbiter.ts +136 -0
- package/src/stem/policy/rule.table.ts +172 -0
- package/src/stem/policy/verdict.recorder.ts +0 -0
- package/src/stem/tracts/biography.writer.ts +3 -3
- package/src/stem/tracts/effector.controller.ts +383 -4
- package/src/stem/tracts/health.reporter.ts +1 -1
- package/src/stem/tracts/outbox.controller.ts +4 -0
- package/src/stem/tracts/outbox.writer.ts +4 -4
- package/src/stem/tracts/pma.controller.ts +1 -1
- package/src/stem/tracts/replay.controller.ts +10 -10
- package/src/stem/tracts/sensory.controller.ts +2 -2
- package/src/stem/tracts/session.logger.ts +4 -4
- package/src/stem/tracts/transport/socketio.transport.ts +7 -7
- package/src/stem/tracts/transport/stream.transport.ts +2 -2
- package/src/stem/tracts/transport.controller.ts +15 -15
|
@@ -23,10 +23,73 @@
|
|
|
23
23
|
|
|
24
24
|
import { logger } from '#core/logger'
|
|
25
25
|
import { reconcileInvocation } from '#agency/reconcile.learning'
|
|
26
|
+
import { NULL_ARBITER, isNullArbiter } from '#stem/policy/arbiter'
|
|
27
|
+
import type { PolicyArbiter, PolicyInvocation, Verdict } from '#stem/policy/arbiter'
|
|
28
|
+
import {
|
|
29
|
+
getVerdictRecorder, getVerdictSource, type PolicyVerdictRecord,
|
|
30
|
+
} from '#stem/policy/verdict.recorder'
|
|
26
31
|
import type { effectorInvocation } from '#types'
|
|
27
32
|
import type { WillInstance } from '#stem/index'
|
|
28
33
|
|
|
34
|
+
/** A denial awaiting application as a refusal ack at the next tick boundary. */
|
|
35
|
+
interface PendingRefusal {
|
|
36
|
+
intentId: string
|
|
37
|
+
schema: string
|
|
38
|
+
reasonCode: string
|
|
39
|
+
finality: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** How long an escalated intent is held awaiting a resolution before it degrades
|
|
43
|
+
* to a refusal — 2× the host-ack timeout, so a human has real time to answer. */
|
|
44
|
+
const ESCALATION_TTL_TICKS = 30
|
|
45
|
+
|
|
46
|
+
/** An escalation the Will has raised: the intent is held, the ask is voiced once,
|
|
47
|
+
* and the original payload is kept so an approval can dispatch it to the world. */
|
|
48
|
+
interface Escalation {
|
|
49
|
+
intentId: string
|
|
50
|
+
schema: string
|
|
51
|
+
reasonCode: string
|
|
52
|
+
/** The withheld invocation payload — replayed to the host on approval. */
|
|
53
|
+
payload: Record<string, unknown>
|
|
54
|
+
expiresAt: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A host's answer to an escalation, applied at the next tick boundary. */
|
|
58
|
+
interface PendingResolution {
|
|
59
|
+
intentId: string
|
|
60
|
+
approved: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
export class effectorController {
|
|
64
|
+
/** The Policy Decision Point consulted before an invocation reaches the world.
|
|
65
|
+
* Defaults to the no-op arbiter, so an unconfigured Will is byte-identical. */
|
|
66
|
+
private _arbiter: PolicyArbiter = NULL_ARBITER
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Denials queued during a step's flush, drained at the NEXT tick boundary
|
|
70
|
+
* (POLICY_REAFFERENCE P1). Keyed by willId — harness state, exactly like
|
|
71
|
+
* `pendingEffectorInvocations`; never simulation state, so it does not touch
|
|
72
|
+
* `simulation.step` determinism and is regenerated on any re-execution.
|
|
73
|
+
*/
|
|
74
|
+
private _pendingRefusals = new Map<string, PendingRefusal[]>()
|
|
75
|
+
|
|
76
|
+
/** Escalations awaiting their first application (mark intent + voice the ask). */
|
|
77
|
+
private _newEscalations = new Map<string, Escalation[]>()
|
|
78
|
+
/** Escalations currently held, keyed by intent id — the resolvable set. */
|
|
79
|
+
private _activeEscalations = new Map<string, Map<string, Escalation>>()
|
|
80
|
+
/** Host answers awaiting application at the next tick boundary. */
|
|
81
|
+
private _pendingResolutions = new Map<string, PendingResolution[]>()
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Install a Policy Decision Point (POLICY_REAFFERENCE P0). Passing null
|
|
85
|
+
* restores the no-op default. The arbiter sees only the proposed act — never
|
|
86
|
+
* simulation state — and its verdict decides whether the invocation is
|
|
87
|
+
* handed to the host at all.
|
|
88
|
+
*/
|
|
89
|
+
setArbiter( arbiter: PolicyArbiter | null ): void {
|
|
90
|
+
this._arbiter = arbiter ?? NULL_ARBITER
|
|
91
|
+
}
|
|
92
|
+
|
|
30
93
|
/**
|
|
31
94
|
* Update the set of allowed communication effectors at runtime via AccessGrants
|
|
32
95
|
* (the permission / sense gate the senses + reply path read).
|
|
@@ -43,6 +106,273 @@ export class effectorController {
|
|
|
43
106
|
* echoes it on its result-ack, and `confirmExecution` uses it to find the intent.
|
|
44
107
|
*/
|
|
45
108
|
bufferInvocation( instance: WillInstance, payload: Record<string, unknown> ): void {
|
|
109
|
+
const willId = instance.config.id
|
|
110
|
+
|
|
111
|
+
// Replay: a registered source re-feeds the recorded verdict instead of
|
|
112
|
+
// re-consulting a live (or absent) PDP — the arbiter is an external oracle,
|
|
113
|
+
// exactly like the LLM. Checked FIRST so replay never re-enters the arbiter.
|
|
114
|
+
const source = getVerdictSource( willId )
|
|
115
|
+
if( source ){
|
|
116
|
+
const invocation = toPolicyInvocation( instance, payload )
|
|
117
|
+
const record = source.verdictFor( invocation.tick, invocation.intentId )
|
|
118
|
+
// A miss means the live run had no verdict here (null arbiter at record
|
|
119
|
+
// time) — the invocation was simply buffered, so reproduce that.
|
|
120
|
+
if( record ) this._applyVerdict( instance, payload, invocation, recordToVerdict( record ) )
|
|
121
|
+
else this._buffer( instance, payload )
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Fast path: no policy configured ⇒ the seam does not exist. No allocation,
|
|
126
|
+
// no branch beyond this one — the byte-identical guarantee.
|
|
127
|
+
if( isNullArbiter( this._arbiter ) ){
|
|
128
|
+
this._buffer( instance, payload )
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const invocation = toPolicyInvocation( instance, payload )
|
|
133
|
+
let verdict: Verdict | Promise<Verdict>
|
|
134
|
+
|
|
135
|
+
// An arbiter that throws must never become an implicit allow.
|
|
136
|
+
try { verdict = this._arbiter.evaluate( invocation ) }
|
|
137
|
+
catch( err ){
|
|
138
|
+
logger.error(`[policy] arbiter "${this._arbiter.name}" threw for "${invocation.schema}" — failing closed:`, err )
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if( verdict instanceof Promise ){
|
|
143
|
+
// An external PDP resolves out of tick. Safe by construction: the executor
|
|
144
|
+
// holds the intent 'awaiting' for AWAIT_TIMEOUT (15 ticks), and the refusal
|
|
145
|
+
// queue drains each tick, so a verdict landing a few ticks late still lands.
|
|
146
|
+
void verdict.then(
|
|
147
|
+
v => this._recordAndApply( instance, payload, invocation, v ),
|
|
148
|
+
err => logger.error(`[policy] arbiter "${this._arbiter.name}" rejected for "${invocation.schema}" — failing closed:`, err ),
|
|
149
|
+
)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this._recordAndApply( instance, payload, invocation, verdict )
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Capture the verdict on the tape (if a recorder is attached), then enforce it. */
|
|
157
|
+
private _recordAndApply(
|
|
158
|
+
instance: WillInstance,
|
|
159
|
+
payload: Record<string, unknown>,
|
|
160
|
+
invocation: PolicyInvocation,
|
|
161
|
+
verdict: Verdict,
|
|
162
|
+
): void {
|
|
163
|
+
const sink = getVerdictRecorder( instance.config.id )
|
|
164
|
+
sink?.recordVerdict({
|
|
165
|
+
tick: invocation.tick,
|
|
166
|
+
willId: instance.config.id,
|
|
167
|
+
intentId: invocation.intentId,
|
|
168
|
+
schema: invocation.schema,
|
|
169
|
+
arbiter: this._arbiter.name,
|
|
170
|
+
decision: verdict.decision,
|
|
171
|
+
...( verdict.reasonCode ? { reasonCode: verdict.reasonCode } : {} ),
|
|
172
|
+
...( verdict.finality ? { finality: verdict.finality } : {} ),
|
|
173
|
+
...( verdict.counterfactual ? { counterfactual: verdict.counterfactual } : {} ),
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
})
|
|
176
|
+
this._applyVerdict( instance, payload, invocation, verdict )
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Enforce a verdict (POLICY_REAFFERENCE P1).
|
|
181
|
+
*
|
|
182
|
+
* • allow → hand the invocation to the world.
|
|
183
|
+
* • deny → queue a refusal ack, applied at the next tick boundary via
|
|
184
|
+
* `confirmExecution` — the same lifecycle as a host rejection,
|
|
185
|
+
* so the mind meets *world resistance*, not a permission dialog.
|
|
186
|
+
* • escalate → raise a held escalation (POLICY_REAFFERENCE P4): the intent is
|
|
187
|
+
* held (the executor stops timing it out), the Will voices a
|
|
188
|
+
* first-person ask once, and a host resolution later approves
|
|
189
|
+
* (dispatch) or denies (refuse). Unresolved, it degrades to a
|
|
190
|
+
* refusal at ESCALATION_TTL_TICKS.
|
|
191
|
+
*
|
|
192
|
+
* P1's refusal reconciles as a plain FAILURE — safe, but the wrong learning
|
|
193
|
+
* signal (forbidden ≠ unskilled). P2 routes it to affordance AVAILABILITY
|
|
194
|
+
* instead of competence.
|
|
195
|
+
*/
|
|
196
|
+
private _applyVerdict(
|
|
197
|
+
instance: WillInstance,
|
|
198
|
+
payload: Record<string, unknown>,
|
|
199
|
+
invocation: PolicyInvocation,
|
|
200
|
+
verdict: Verdict,
|
|
201
|
+
): void {
|
|
202
|
+
if( verdict.decision === 'allow'){
|
|
203
|
+
this._buffer( instance, payload )
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const cf = verdict.counterfactual
|
|
208
|
+
logger.info(
|
|
209
|
+
`[policy] ${verdict.decision.toUpperCase()} "${invocation.schema}" intent "${invocation.intentId}"` +
|
|
210
|
+
` — ${verdict.reasonCode ?? 'no reason code'}` +
|
|
211
|
+
( verdict.finality ? ` (${verdict.finality})` : '') +
|
|
212
|
+
( cf ? ` [${cf.field}: requested ${JSON.stringify( cf.requested )}, allowed ${JSON.stringify( cf.allowed )}]` : ''),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if( verdict.decision === 'deny'){
|
|
216
|
+
const queue = this._pendingRefusals.get( instance.config.id ) ?? []
|
|
217
|
+
queue.push({
|
|
218
|
+
intentId: invocation.intentId,
|
|
219
|
+
schema: invocation.schema,
|
|
220
|
+
reasonCode: verdict.reasonCode ?? 'POLICY_DENIED',
|
|
221
|
+
finality: verdict.finality ?? 'instance',
|
|
222
|
+
})
|
|
223
|
+
this._pendingRefusals.set( instance.config.id, queue )
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 'escalate' — raise a held escalation, applied (marked + voiced) at the boundary.
|
|
228
|
+
const escalations = this._newEscalations.get( instance.config.id ) ?? []
|
|
229
|
+
escalations.push({
|
|
230
|
+
intentId: invocation.intentId,
|
|
231
|
+
schema: invocation.schema,
|
|
232
|
+
reasonCode: verdict.reasonCode ?? 'APPROVAL_REQUIRED',
|
|
233
|
+
payload,
|
|
234
|
+
expiresAt: 0, // stamped when applied (we don't have the current tick here)
|
|
235
|
+
})
|
|
236
|
+
this._newEscalations.set( instance.config.id, escalations )
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Record a host's answer to an escalation (POLICY_REAFFERENCE P4). Applied at
|
|
241
|
+
* the next tick boundary so every simulation-state write stays on the boundary:
|
|
242
|
+
* approve dispatches the held invocation to the world; deny refuses it. A
|
|
243
|
+
* no-op if the intent id is not (or no longer) an active escalation.
|
|
244
|
+
*/
|
|
245
|
+
resolveEscalation( instance: WillInstance, intentId: string, approved: boolean ): void {
|
|
246
|
+
const queue = this._pendingResolutions.get( instance.config.id ) ?? []
|
|
247
|
+
queue.push({ intentId, approved })
|
|
248
|
+
this._pendingResolutions.set( instance.config.id, queue )
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Apply queued policy refusals as failure acks (POLICY_REAFFERENCE P1).
|
|
253
|
+
* Called by the tick loop at the same boundary as inbound acks — BEFORE the
|
|
254
|
+
* step, stamped to this tick — so a denial reconciled here is the exact
|
|
255
|
+
* lifecycle of a host rejection that arrived between ticks.
|
|
256
|
+
*/
|
|
257
|
+
applyPolicyOutcomes( instance: WillInstance ): void {
|
|
258
|
+
const tick = instance.tickCount
|
|
259
|
+
this._applyResolutions( instance ) // host answers land first
|
|
260
|
+
this._expireEscalations( instance, tick ) // then time out the unanswered
|
|
261
|
+
this._applyNewEscalations( instance, tick ) // then raise + voice the newest
|
|
262
|
+
this._applyRefusals( instance ) // then the plain denials
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Drain queued refusals into failure acks (POLICY_REAFFERENCE P1). */
|
|
266
|
+
private _applyRefusals( instance: WillInstance ): void {
|
|
267
|
+
const queue = this._pendingRefusals.get( instance.config.id )
|
|
268
|
+
if( !queue || queue.length === 0 ) return
|
|
269
|
+
this._pendingRefusals.set( instance.config.id, [] )
|
|
270
|
+
|
|
271
|
+
for( const refusal of queue )
|
|
272
|
+
this.confirmExecution( instance, refusal.intentId, {
|
|
273
|
+
success: false,
|
|
274
|
+
refused: true,
|
|
275
|
+
finality: refusal.finality === 'class' ? 'class' : 'instance',
|
|
276
|
+
description: `refused by policy: ${refusal.reasonCode} (${refusal.finality})`,
|
|
277
|
+
} )
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Raise each newly-escalated intent (POLICY_REAFFERENCE P4): mark it held in
|
|
281
|
+
* simulation state, voice the ask ONCE, and move it to the resolvable set. */
|
|
282
|
+
private _applyNewEscalations( instance: WillInstance, tick: number ): void {
|
|
283
|
+
const pending = this._newEscalations.get( instance.config.id )
|
|
284
|
+
if( !pending || pending.length === 0 ) return
|
|
285
|
+
this._newEscalations.set( instance.config.id, [] )
|
|
286
|
+
|
|
287
|
+
const active = this._activeEscalations.get( instance.config.id ) ?? new Map<string, Escalation>()
|
|
288
|
+
for( const esc of pending ){
|
|
289
|
+
esc.expiresAt = tick + ESCALATION_TTL_TICKS
|
|
290
|
+
this._markEscalated( instance, esc.intentId, esc.expiresAt )
|
|
291
|
+
this._voiceEscalation( instance, esc )
|
|
292
|
+
active.set( esc.intentId, esc )
|
|
293
|
+
}
|
|
294
|
+
this._activeEscalations.set( instance.config.id, active )
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Apply host answers to active escalations (POLICY_REAFFERENCE P4). */
|
|
298
|
+
private _applyResolutions( instance: WillInstance ): void {
|
|
299
|
+
const queue = this._pendingResolutions.get( instance.config.id )
|
|
300
|
+
if( !queue || queue.length === 0 ) return
|
|
301
|
+
this._pendingResolutions.set( instance.config.id, [] )
|
|
302
|
+
|
|
303
|
+
const active = this._activeEscalations.get( instance.config.id )
|
|
304
|
+
for( const { intentId, approved } of queue ){
|
|
305
|
+
const esc = active?.get( intentId )
|
|
306
|
+
if( !esc ) continue // unknown / already resolved — ignore
|
|
307
|
+
active!.delete( intentId )
|
|
308
|
+
this._clearEscalated( instance, intentId ) // release the executor's hold
|
|
309
|
+
if( approved ){
|
|
310
|
+
this._buffer( instance, esc.payload ) // dispatch the held invocation now
|
|
311
|
+
logger.info(`[policy] escalation APPROVED → dispatching "${esc.schema}" intent "${intentId}"`)
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
this._queueRefusal( instance, esc.intentId, esc.schema, esc.reasonCode, 'class')
|
|
315
|
+
logger.info(`[policy] escalation DENIED → refusing "${esc.schema}" intent "${intentId}"`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Degrade escalations no one answered in time into instance-refusals (P4). */
|
|
321
|
+
private _expireEscalations( instance: WillInstance, tick: number ): void {
|
|
322
|
+
const active = this._activeEscalations.get( instance.config.id )
|
|
323
|
+
if( !active || active.size === 0 ) return
|
|
324
|
+
for( const [ intentId, esc ] of active ){
|
|
325
|
+
if( tick < esc.expiresAt ) continue
|
|
326
|
+
active.delete( intentId )
|
|
327
|
+
this._clearEscalated( instance, intentId )
|
|
328
|
+
this._queueRefusal( instance, esc.intentId, esc.schema, 'ESCALATION_EXPIRED', 'instance')
|
|
329
|
+
logger.info(`[policy] escalation EXPIRED → refusing "${esc.schema}" intent "${intentId}"`)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Push a refusal onto the queue drained by _applyRefusals this same tick. */
|
|
334
|
+
private _queueRefusal(
|
|
335
|
+
instance: WillInstance, intentId: string, schema: string, reasonCode: string, finality: 'class' | 'instance',
|
|
336
|
+
): void {
|
|
337
|
+
const queue = this._pendingRefusals.get( instance.config.id ) ?? []
|
|
338
|
+
queue.push({ intentId, schema, reasonCode, finality })
|
|
339
|
+
this._pendingRefusals.set( instance.config.id, queue )
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Mark the awaiting intent held: the executor stops timing it out (P4). */
|
|
343
|
+
private _markEscalated( instance: WillInstance, intentId: string, expiresAt: number ): void {
|
|
344
|
+
const intent = instance.simulation.stateManager.snapshot().entities.get( intentId )
|
|
345
|
+
if( !intent || intent.type !== 'agency.intent') return
|
|
346
|
+
instance.simulation.stateManager.setEntity({
|
|
347
|
+
id: intent.id,
|
|
348
|
+
type: intent.type,
|
|
349
|
+
metadata: { ...( intent.metadata ?? {} ), escalated: true, escalationExpiresAt: expiresAt },
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Release the hold so the executor resumes normal timeout for this intent. */
|
|
354
|
+
private _clearEscalated( instance: WillInstance, intentId: string ): void {
|
|
355
|
+
const intent = instance.simulation.stateManager.snapshot().entities.get( intentId )
|
|
356
|
+
if( !intent || intent.type !== 'agency.intent') return
|
|
357
|
+
const meta = { ...( intent.metadata ?? {} ) } as Record<string, unknown>
|
|
358
|
+
delete meta['escalated']; delete meta['escalationExpiresAt']
|
|
359
|
+
instance.simulation.stateManager.setEntity({ id: intent.id, type: intent.type, metadata: meta })
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Voice the escalation as a first-person broadcast ask — once, at raise time. */
|
|
363
|
+
private _voiceEscalation( instance: WillInstance, esc: Escalation ): void {
|
|
364
|
+
try {
|
|
365
|
+
instance.cognition.outboxWriter.enqueue({
|
|
366
|
+
targetEntityId: '*',
|
|
367
|
+
content: escalationAsk( esc.schema, esc.reasonCode ),
|
|
368
|
+
effectorName: 'broadcast',
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
catch( err ){ logger.warn(`[policy] escalation voice failed for "${esc.schema}": ${errMsg( err )}`) }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Queue an approved invocation for the delivery layer. */
|
|
375
|
+
private _buffer( instance: WillInstance, payload: Record<string, unknown> ): void {
|
|
46
376
|
const intentId = ( payload.intentId as string ) ?? ''
|
|
47
377
|
instance.pendingEffectorInvocations.push({
|
|
48
378
|
id: intentId,
|
|
@@ -86,16 +416,20 @@ export class effectorController {
|
|
|
86
416
|
success: boolean
|
|
87
417
|
description: string
|
|
88
418
|
metrics?: Record<string, number>
|
|
419
|
+
/** POLICY_REAFFERENCE P2 — set when the ack is a policy refusal, so the
|
|
420
|
+
* ReafferenceEngine routes it to availability rather than competence. */
|
|
421
|
+
refused?: boolean
|
|
422
|
+
finality?: 'class' | 'instance'
|
|
89
423
|
},
|
|
90
424
|
): void {
|
|
91
425
|
const tick = instance.tickCount
|
|
92
426
|
|
|
93
427
|
const intent = instance.simulation.stateManager.snapshot().entities.get( invocationId )
|
|
94
|
-
if( !intent || intent.type !== 'agency.intent'
|
|
428
|
+
if( !intent || intent.type !== 'agency.intent'){
|
|
95
429
|
// No awaiting intent for this id. Expected when the executor's 15-tick await
|
|
96
430
|
// timeout already abandoned it (writing a failed outcome + advancing any plan)
|
|
97
431
|
// before the host's late ack arrived, or the id is unknown. Drop the straggler.
|
|
98
|
-
logger.warn(
|
|
432
|
+
logger.warn(`[effector] confirmExecution: no awaiting agency.intent "${invocationId}" (timed out / already reconciled?) — ignored`)
|
|
99
433
|
return
|
|
100
434
|
}
|
|
101
435
|
|
|
@@ -122,7 +456,7 @@ export class effectorController {
|
|
|
122
456
|
if( typeof v === 'number' && Number.isFinite( v ) )
|
|
123
457
|
instance.simulation.stateManager.setMetric( k, v )
|
|
124
458
|
else
|
|
125
|
-
logger.warn(
|
|
459
|
+
logger.warn(`[effector] confirmExecution: dropped non-finite metric "${k}"=${String( v )} from host ack (${schema})`)
|
|
126
460
|
}
|
|
127
461
|
|
|
128
462
|
instance.sessionLogger?.write({
|
|
@@ -135,10 +469,55 @@ export class effectorController {
|
|
|
135
469
|
confirmedExternally: true,
|
|
136
470
|
} as never)
|
|
137
471
|
|
|
138
|
-
logger.info(
|
|
472
|
+
logger.info(`[effector] ✓ reconciled ${result.success ? 'success' : 'failure'}: intent "${invocationId}" (${schema})`)
|
|
139
473
|
}
|
|
140
474
|
}
|
|
141
475
|
|
|
142
476
|
function num( v: unknown, fallback: number ): number {
|
|
143
477
|
return typeof v === 'number' && Number.isFinite( v ) ? v : fallback
|
|
144
478
|
}
|
|
479
|
+
|
|
480
|
+
/** First-person ask for an escalated action, carrying the reason's MEANING (P4).
|
|
481
|
+
* Kept template-simple here; the facet-authored version is a later refinement. */
|
|
482
|
+
function escalationAsk( schema: string, reasonCode: string ): string {
|
|
483
|
+
const meaning = ESCALATION_MEANINGS[ reasonCode ] ?? 'I need your approval before I can do this'
|
|
484
|
+
return `I want to ${ schema }, but ${ meaning }. May I go ahead?`
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** reasonCode → human meaning. Unknown codes fall back to a generic phrase. */
|
|
488
|
+
const ESCALATION_MEANINGS: Record<string, string> = {
|
|
489
|
+
APPROVAL_REQUIRED: 'I need your approval before I can on my own',
|
|
490
|
+
WRITE_REQUIRES_APPROVAL: "it writes to the world and I shouldn't on my own",
|
|
491
|
+
PAYMENT_REQUIRES_APPROVAL: 'it moves money and I must not do that unattended',
|
|
492
|
+
DEPLOY_REQUIRES_APPROVAL: 'it ships something and needs a human to sign off',
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function errMsg( err: unknown ): string {
|
|
496
|
+
return err instanceof Error ? err.message : String( err )
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** Reconstruct an enforceable Verdict from a recorded verdict (replay path). */
|
|
500
|
+
function recordToVerdict( record: PolicyVerdictRecord ): Verdict {
|
|
501
|
+
return {
|
|
502
|
+
decision: record.decision,
|
|
503
|
+
...( record.reasonCode ? { reasonCode: record.reasonCode } : {} ),
|
|
504
|
+
...( record.finality ? { finality: record.finality } : {} ),
|
|
505
|
+
...( record.counterfactual ? { counterfactual: record.counterfactual } : {} ),
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Project the `agency.invocation` payload onto the policy boundary's view of a
|
|
511
|
+
* proposed act. Only the act crosses — no cognitive internals, no state handle.
|
|
512
|
+
*/
|
|
513
|
+
function toPolicyInvocation( instance: WillInstance, payload: Record<string, unknown> ): PolicyInvocation {
|
|
514
|
+
return {
|
|
515
|
+
willId: instance.config.id,
|
|
516
|
+
intentId: ( payload.intentId as string ) ?? '',
|
|
517
|
+
schema: ( payload.schema as string ) ?? '',
|
|
518
|
+
parameters: ( payload.parameters as Record<string, unknown> ) ?? {},
|
|
519
|
+
...( typeof payload.targetEntityId === 'string' ? { targetEntityId: payload.targetEntityId } : {} ),
|
|
520
|
+
...( typeof payload.description === 'string' ? { description: payload.description } : {} ),
|
|
521
|
+
tick: ( payload.tick as number ) ?? 0,
|
|
522
|
+
}
|
|
523
|
+
}
|
|
@@ -87,7 +87,7 @@ export class HealthReporter {
|
|
|
87
87
|
for( const entity of instance.simulation.stateManager.getEntitiesByType('goal') ){
|
|
88
88
|
totalGoals++
|
|
89
89
|
const s = entity.metadata?.status
|
|
90
|
-
if( s === 'active' || s === 'in_progress'
|
|
90
|
+
if( s === 'active' || s === 'in_progress') activeGoals++
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
const goalScore = totalGoals === 0
|
|
@@ -104,6 +104,10 @@ export class OutboxController {
|
|
|
104
104
|
: `My message failed to reach the recipient.`,
|
|
105
105
|
salience: delivered ? 0.35 : 0.6,
|
|
106
106
|
changeType: delivered ? 'delivered' : 'failed',
|
|
107
|
+
// Reafference by construction — this percept describes our own action's
|
|
108
|
+
// outcome ("ear hears the word"). Tagged, not attenuated: it is the ack
|
|
109
|
+
// surface, not a content echo (EXAFFERENCE P2).
|
|
110
|
+
provenance: 'reafferent',
|
|
107
111
|
messageId,
|
|
108
112
|
},
|
|
109
113
|
})
|
|
@@ -53,7 +53,7 @@ export class OutboxWriter {
|
|
|
53
53
|
this._sessionLogger = logger
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
private _genId( suffix = ''
|
|
56
|
+
private _genId( suffix = ''): string {
|
|
57
57
|
return `outbox-${ this._willId }-${ ++this._seq }${ suffix }`
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -61,7 +61,7 @@ export class OutboxWriter {
|
|
|
61
61
|
* Push one canonical outbox row and return its generated id. The single point
|
|
62
62
|
* where the row shape is materialized — all producers funnel through here.
|
|
63
63
|
*/
|
|
64
|
-
enqueue( row: OutboxRow, idSuffix = ''
|
|
64
|
+
enqueue( row: OutboxRow, idSuffix = ''): string {
|
|
65
65
|
const id = this._genId( idSuffix )
|
|
66
66
|
this._outbox.push({
|
|
67
67
|
id,
|
|
@@ -105,7 +105,7 @@ export class OutboxWriter {
|
|
|
105
105
|
|
|
106
106
|
const ids: string[] = []
|
|
107
107
|
bubbles.forEach( ( bubble, i ) => {
|
|
108
|
-
if( !pushToOutbox ){ ids.push( this._genId(
|
|
108
|
+
if( !pushToOutbox ){ ids.push( this._genId(`-${ i }`) ); return }
|
|
109
109
|
logger.info(`[outbox-writer] reply → ${ entityId } bubble[${ i }] "${ bubble.slice( 0, 80 ) }"`)
|
|
110
110
|
ids.push( this.enqueue({
|
|
111
111
|
targetEntityId: entityId,
|
|
@@ -113,7 +113,7 @@ export class OutboxWriter {
|
|
|
113
113
|
content: bubble,
|
|
114
114
|
effectorName: 'text',
|
|
115
115
|
threadId,
|
|
116
|
-
}, `-${ i }`
|
|
116
|
+
}, `-${ i }`) )
|
|
117
117
|
} )
|
|
118
118
|
|
|
119
119
|
this._sessionLogger?.write({
|
|
@@ -29,7 +29,7 @@ export class PMAController {
|
|
|
29
29
|
* with existing beliefs may cause unintended merges.
|
|
30
30
|
*/
|
|
31
31
|
load( id: string, instance: WillInstance, pma: PMASnapshot ): void {
|
|
32
|
-
if( instance.status === 'active'
|
|
32
|
+
if( instance.status === 'active')
|
|
33
33
|
throw new Error(`Cannot load PMA into an active Will (${id}). Pause first.`)
|
|
34
34
|
|
|
35
35
|
// Re-validate the artifact's persona at the load boundary (Phase 2): a stored
|
|
@@ -44,7 +44,7 @@ export class ReplayController {
|
|
|
44
44
|
// Pre-create the replay dir so flush() can persist segments incrementally
|
|
45
45
|
// (stop() saves the consolidated file into the same dir).
|
|
46
46
|
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
47
|
-
const dir = join( dataDir, 'wills', id, 'replays'
|
|
47
|
+
const dir = join( dataDir, 'wills', id, 'replays')
|
|
48
48
|
mkdirSync( dir, { recursive: true } )
|
|
49
49
|
|
|
50
50
|
const recorder = this._replayManager.createRecorder( id, runId, undefined, {
|
|
@@ -74,10 +74,10 @@ export class ReplayController {
|
|
|
74
74
|
this._activeRecorders.delete( id )
|
|
75
75
|
|
|
76
76
|
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
77
|
-
const dir = join( dataDir, 'wills', id, 'replays'
|
|
77
|
+
const dir = join( dataDir, 'wills', id, 'replays')
|
|
78
78
|
mkdirSync( dir, { recursive: true } )
|
|
79
79
|
|
|
80
|
-
const path = join( dir, `${active.runId}.json`
|
|
80
|
+
const path = join( dir, `${active.runId}.json`)
|
|
81
81
|
await active.recorder.save( path )
|
|
82
82
|
|
|
83
83
|
const meta = active.recorder.getMetadata()
|
|
@@ -97,11 +97,11 @@ export class ReplayController {
|
|
|
97
97
|
if( cached ) return cached
|
|
98
98
|
|
|
99
99
|
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
100
|
-
const path = join( dataDir, 'wills', id, 'replays', `${runId}.json`
|
|
100
|
+
const path = join( dataDir, 'wills', id, 'replays', `${runId}.json`)
|
|
101
101
|
if( !existsSync( path ) ) return null
|
|
102
102
|
|
|
103
103
|
try {
|
|
104
|
-
const meta = ( JSON.parse( readFileSync( path, 'utf8'
|
|
104
|
+
const meta = ( JSON.parse( readFileSync( path, 'utf8') ) as { metadata: ReplayMetadata } ).metadata
|
|
105
105
|
this._completedReplays.set( key, meta )
|
|
106
106
|
this._replayPaths.set( key, path )
|
|
107
107
|
return meta
|
|
@@ -121,7 +121,7 @@ export class ReplayController {
|
|
|
121
121
|
if( key.startsWith( prefix ) ) results.push( meta )
|
|
122
122
|
|
|
123
123
|
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
124
|
-
const dir = join( dataDir, 'wills', id, 'replays'
|
|
124
|
+
const dir = join( dataDir, 'wills', id, 'replays')
|
|
125
125
|
|
|
126
126
|
if( existsSync( dir ) ){
|
|
127
127
|
const inMem = new Set(
|
|
@@ -137,7 +137,7 @@ export class ReplayController {
|
|
|
137
137
|
if( inMem.has( runId ) ) continue
|
|
138
138
|
try {
|
|
139
139
|
const path = join( dir, file )
|
|
140
|
-
const meta = ( JSON.parse( readFileSync( path, 'utf8'
|
|
140
|
+
const meta = ( JSON.parse( readFileSync( path, 'utf8') ) as { metadata: ReplayMetadata } ).metadata
|
|
141
141
|
const key = `${id}:${runId}`
|
|
142
142
|
this._completedReplays.set( key, meta )
|
|
143
143
|
this._replayPaths.set( key, path )
|
|
@@ -164,14 +164,14 @@ export class ReplayController {
|
|
|
164
164
|
* process (e.g. after a server restart).
|
|
165
165
|
*/
|
|
166
166
|
private _resolvePath( id: string, runId: string ): string | undefined {
|
|
167
|
-
const cached = this._replayPaths.get(
|
|
167
|
+
const cached = this._replayPaths.get(`${id}:${runId}`)
|
|
168
168
|
if( cached ) return cached
|
|
169
169
|
|
|
170
170
|
const dataDir = process.env[ 'WILL_DATA_DIR' ] ?? './data'
|
|
171
|
-
const path = join( dataDir, 'wills', id, 'replays', `${runId}.json`
|
|
171
|
+
const path = join( dataDir, 'wills', id, 'replays', `${runId}.json`)
|
|
172
172
|
if( !existsSync( path ) ) return undefined
|
|
173
173
|
|
|
174
|
-
this._replayPaths.set(
|
|
174
|
+
this._replayPaths.set(`${id}:${runId}`, path )
|
|
175
175
|
return path
|
|
176
176
|
}
|
|
177
177
|
}
|
|
@@ -52,7 +52,7 @@ export class SensoryController {
|
|
|
52
52
|
gustation: cog.gustationEngine,
|
|
53
53
|
}
|
|
54
54
|
const engine = engineMap[ domain ]
|
|
55
|
-
if( !engine ) throw Object.assign( new Error(
|
|
55
|
+
if( !engine ) throw Object.assign( new Error(`Unknown sense domain: ${domain}`), { code: 400 } )
|
|
56
56
|
await engine.ingest( input )
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -135,7 +135,7 @@ export class SensoryController {
|
|
|
135
135
|
private _ensureChunkFanout( instance: WillInstance ): void {
|
|
136
136
|
if( this._chunkWired.has( instance ) ) return
|
|
137
137
|
const audition = instance.cognition.auditionEngine
|
|
138
|
-
if( typeof ( audition as { addChunkCallback?: unknown } ).addChunkCallback !== 'function'
|
|
138
|
+
if( typeof ( audition as { addChunkCallback?: unknown } ).addChunkCallback !== 'function') return
|
|
139
139
|
|
|
140
140
|
this._chunkWired.add( instance )
|
|
141
141
|
audition.addChunkCallback( ( entityId: string, _threadId: string, chunk: string ) => {
|
|
@@ -97,7 +97,7 @@ export class SessionLogger {
|
|
|
97
97
|
private _count = 0
|
|
98
98
|
|
|
99
99
|
constructor( willId: string, dataDir: string = './data', opts: SessionLoggerOptions = {} ) {
|
|
100
|
-
const ts = new Date().toISOString().replace( /[:.]/g, '-'
|
|
100
|
+
const ts = new Date().toISOString().replace( /[:.]/g, '-').slice( 0, 19 )
|
|
101
101
|
const rnd = Math.random().toString( 36 ).slice( 2, 8 )
|
|
102
102
|
this.sessionId = `${ts}-${rnd}`
|
|
103
103
|
|
|
@@ -105,9 +105,9 @@ export class SessionLogger {
|
|
|
105
105
|
|
|
106
106
|
const fileLogging = opts.fileLogging ?? false
|
|
107
107
|
if( fileLogging ){
|
|
108
|
-
const dir = join( opts.dataDir ?? dataDir, 'wills', willId, 'sessions'
|
|
108
|
+
const dir = join( opts.dataDir ?? dataDir, 'wills', willId, 'sessions')
|
|
109
109
|
mkdirSync( dir, { recursive: true } )
|
|
110
|
-
this.filePath = join( dir, `${this.sessionId}.jsonl`
|
|
110
|
+
this.filePath = join( dir, `${this.sessionId}.jsonl`)
|
|
111
111
|
this._stream = createWriteStream( this.filePath, { flags: 'a' } )
|
|
112
112
|
} else {
|
|
113
113
|
this.filePath = ''
|
|
@@ -133,7 +133,7 @@ export class SessionLogger {
|
|
|
133
133
|
|
|
134
134
|
// File is a dev-only mirror.
|
|
135
135
|
if( this._stream ){
|
|
136
|
-
try { this._stream.write( JSON.stringify( record ) + '\n'
|
|
136
|
+
try { this._stream.write( JSON.stringify( record ) + '\n') }
|
|
137
137
|
catch { /* never block the tick loop on a log failure */ }
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -91,7 +91,7 @@ export class SocketIoTransport implements ExternalTransport {
|
|
|
91
91
|
const timer = setTimeout( () => done({ acked: false, via: 'timeout' }), timeoutMs )
|
|
92
92
|
|
|
93
93
|
try {
|
|
94
|
-
socket.emit(
|
|
94
|
+
socket.emit('envelope', stamped, ( response: unknown ) => done({ acked: true, via: 'callback', payload: response }) )
|
|
95
95
|
}
|
|
96
96
|
catch( err ){
|
|
97
97
|
logger.warn(`[socketio-transport] emit failed: ${( err as Error ).message}`)
|
|
@@ -159,20 +159,20 @@ export class SocketIoTransport implements ExternalTransport {
|
|
|
159
159
|
|
|
160
160
|
/** Attach socket.io event handlers that feed the inbound + status streams. */
|
|
161
161
|
private _wire( socket: SocketLike ): void {
|
|
162
|
-
socket.on(
|
|
163
|
-
socket.on(
|
|
164
|
-
socket.on(
|
|
162
|
+
socket.on('connect', () => this._notifyStatus('connected') )
|
|
163
|
+
socket.on('disconnect', () => this._notifyStatus('disconnected') )
|
|
164
|
+
socket.on('reconnect_attempt', () => this._notifyStatus('reconnecting') )
|
|
165
165
|
|
|
166
166
|
// Primary inbound channel — already-typed envelopes from the peer.
|
|
167
|
-
socket.on(
|
|
167
|
+
socket.on('envelope', ( env: InboundEnvelope ) => this._emitInbound( env ) )
|
|
168
168
|
|
|
169
169
|
// Discrete ack events (Section 2): in case the peer emits acks independently
|
|
170
170
|
// rather than via the emit() callback. Synthesized into ack envelopes; the
|
|
171
171
|
// AckReconciler (Section 3) dedups against the callback path by correlationId.
|
|
172
|
-
socket.on(
|
|
172
|
+
socket.on('message.delivered', ( m: { correlationId: string; delivered?: boolean } ) =>
|
|
173
173
|
this._emitInbound( this._deliveryAck( m.correlationId, m.delivered ?? true ) ) )
|
|
174
174
|
|
|
175
|
-
socket.on(
|
|
175
|
+
socket.on('effector.invoked.ack', ( m: { correlationId: string; result: AckEnvelope['result'] } ) =>
|
|
176
176
|
this._emitInbound( this._resultAck( m.correlationId, m.result ) ) )
|
|
177
177
|
}
|
|
178
178
|
|