@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
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// src/agency/conversation.aim.ts — did the words achieve what they were for?
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// A communicative act has an AIM that is not its EXECUTION. Sending succeeded;
|
|
6
|
+
// that is a fact about the channel. Whether anyone answered is a fact about the
|
|
7
|
+
// world, it arrives later, and until this module existed nothing in the mind
|
|
8
|
+
// represented it.
|
|
9
|
+
//
|
|
10
|
+
// The cost of not representing it, measured on a live Will: `agency.skill` for
|
|
11
|
+
// `reach-out` read `enactments 28, successes 28` — a 100% success rate, because
|
|
12
|
+
// the outbox accepted every one. Habit (0.78) and value (0.5) therefore rose with
|
|
13
|
+
// each repetition while the only opposing term, satiation, is bounded and decays.
|
|
14
|
+
// The mind asked one person the same question eleven times in two and a half
|
|
15
|
+
// minutes and, from the inside, every one of them was the first: it could see
|
|
16
|
+
// `✓ reach-out` in its recent outcomes and nothing at all about being ignored.
|
|
17
|
+
//
|
|
18
|
+
// So this is not a rule against repeating. It is the missing PERCEPT. The mind
|
|
19
|
+
// gets to know that it spoke and was not answered; what to do about that is its
|
|
20
|
+
// own business — a person who has been ignored twice may well try a third time,
|
|
21
|
+
// and should be able to, having noticed.
|
|
22
|
+
//
|
|
23
|
+
// Everything here is pure and tick-denominated: no wall clock, no RNG, no state
|
|
24
|
+
// writes. Ids and decisions stay identical between a recorded run and its replay
|
|
25
|
+
// (R2). The resolution is folded into state by ReafferenceEngine, which already
|
|
26
|
+
// owns the question "did the world confirm my act?".
|
|
27
|
+
// ─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
import type { Tick } from '#core/types'
|
|
30
|
+
import { readAliases, canonicalOf } from '#cognition/social.identity'
|
|
31
|
+
|
|
32
|
+
export const SENT_TYPE = 'conversation.sent'
|
|
33
|
+
export const RECEIVED_TYPE = 'conversation.received'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How long the mind waits before it counts a silence as an answer in itself.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately much longer than the echo window (`CONSEQUENCE_TTL_TICKS`, 30), a
|
|
39
|
+
* motor await (`AWAIT_TIMEOUT`, 15), or satiation (`repeatWindowTicks`, 60).
|
|
40
|
+
* Those ask "did my act leave the building?" and "does saying it again feel right
|
|
41
|
+
* yet?"; this asks "did a person get back to me?", and people take their time. At
|
|
42
|
+
* the 1s tick a hosted Will actually runs on, 240 is four minutes — long enough
|
|
43
|
+
* that a silence is genuinely a silence and not somebody typing.
|
|
44
|
+
*
|
|
45
|
+
* Tuned through the persona prior rather than fixed, because how long a quiet
|
|
46
|
+
* takes to mean something is a trait — the same reason `facetIdleTtlTicks` and
|
|
47
|
+
* `repeatWindowTicks` are not constants.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_REPLY_WINDOW_TICKS = 240
|
|
50
|
+
|
|
51
|
+
/** Minimal shape this module needs off a state entity — keeps it testable without a StateManager. */
|
|
52
|
+
export interface EntityLike {
|
|
53
|
+
type: string
|
|
54
|
+
/**
|
|
55
|
+
* The real field. `StateManager.setEntity` stamps it from the sim clock on
|
|
56
|
+
* every write, and it is the ONLY tick a record written off-tick can have —
|
|
57
|
+
* an inbound turn arrives between ticks and its writer has no clock to quote.
|
|
58
|
+
*/
|
|
59
|
+
updatedAtTick?: number
|
|
60
|
+
/** Not on `SimulationEntity`; accepted so a test can state a tick directly. */
|
|
61
|
+
tick?: number
|
|
62
|
+
metadata?: ReadonlyMap<string, unknown> | Record<string, unknown>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* When a record happened: what its writer declared, else when the state manager
|
|
67
|
+
* stamped it.
|
|
68
|
+
*
|
|
69
|
+
* `SimulationEntity` has no `tick` — it is `updatedAtTick`, and reading the
|
|
70
|
+
* former silently yields `undefined` on every real entity. That is not a
|
|
71
|
+
* hypothetical: this module shipped reading `e.tick`, so `lastHeard` was 0 for
|
|
72
|
+
* everybody, `0 > sentTick` was never true, and NO turn could ever be marked
|
|
73
|
+
* answered. Caught on a live run where she was mid-conversation and the engine
|
|
74
|
+
* announced "no answer from Fabrice" about a message he had already replied to.
|
|
75
|
+
*
|
|
76
|
+
* The same dead read sits in `spokenAtByEntity` (agency/consequence.ts) behind a
|
|
77
|
+
* comment explaining why the fallback matters. It never fired there either.
|
|
78
|
+
*/
|
|
79
|
+
export function tickOf( e: EntityLike, meta: Record<string, unknown> ): Tick {
|
|
80
|
+
return ( num( meta['tick'] ) ?? num( e.updatedAtTick ) ?? num( e.tick ) ?? 0 ) as Tick
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** One thing the mind said to one person, and what became of it. */
|
|
84
|
+
export interface SpokenTurn {
|
|
85
|
+
entityId: string
|
|
86
|
+
targetEntityId: string
|
|
87
|
+
targetEntityName?: string
|
|
88
|
+
preview: string
|
|
89
|
+
tick: Tick
|
|
90
|
+
/** Set once the target has spoken after this turn. */
|
|
91
|
+
answeredAt?: Tick
|
|
92
|
+
/**
|
|
93
|
+
* WHAT they said back.
|
|
94
|
+
*
|
|
95
|
+
* Recorded on the turn rather than looked up later, because a
|
|
96
|
+
* `conversation.received` lives exactly one tick — SocialPerception sweeps it as
|
|
97
|
+
* a one-shot event — so the text is unavailable by the next render. Without it
|
|
98
|
+
* the mind is told an answer ARRIVED and never shown it, which is worse than
|
|
99
|
+
* silence: it invites the mind to act as though it has the answer. Live, that
|
|
100
|
+
* put a wrong meeting time in front of a third party — she asked "same time,
|
|
101
|
+
* 3pm?", was told `they answered`, never saw the correction to 2pm, and relayed
|
|
102
|
+
* 3pm as confirmed.
|
|
103
|
+
*/
|
|
104
|
+
answeredWith?: string
|
|
105
|
+
/** Set once the reply window closed on a silence — recorded so it is announced once, not every tick. */
|
|
106
|
+
unansweredAt?: Tick
|
|
107
|
+
/** An acknowledgement closes a turn rather than opening one; it awaits nothing. */
|
|
108
|
+
isAck: boolean
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function meta( e: EntityLike ): Record<string, unknown> {
|
|
112
|
+
const m = e.metadata
|
|
113
|
+
if( !m ) return {}
|
|
114
|
+
return m instanceof Map ? Object.fromEntries( m ) : m as Record<string, unknown>
|
|
115
|
+
}
|
|
116
|
+
function str( v: unknown ): string | undefined { return typeof v === 'string' ? v : undefined }
|
|
117
|
+
function num( v: unknown ): number | undefined {
|
|
118
|
+
return typeof v === 'number' && Number.isFinite( v ) ? v : undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Every `conversation.sent` in state, as turns.
|
|
123
|
+
*
|
|
124
|
+
* The tick comes from `metadata.tick` where the writer set one, else the entity's
|
|
125
|
+
* own tick — `StateManager.setEntity` stamps that from the sim clock on every
|
|
126
|
+
* write. The fallback is not decoration: the two writers (ProactiveCommunicator
|
|
127
|
+
* and AuditionEngine) disagree about which they fill, and defaulting to 0 made
|
|
128
|
+
* every record of the second kind look infinitely old. Same rule as
|
|
129
|
+
* `spokenAtByEntity`, deliberately.
|
|
130
|
+
*/
|
|
131
|
+
export function readSpokenTurns( entities: ReadonlyMap<string, EntityLike> ): SpokenTurn[] {
|
|
132
|
+
// Through the alias table, because the two writers name their target in
|
|
133
|
+
// different id spaces: a REPLY is addressed to the transport id the percept
|
|
134
|
+
// arrived with (`discord:1019…`), while a PROACTIVE message is addressed to the
|
|
135
|
+
// anchor the executive resolved (`ke:…`). Both are the same someone. Matched
|
|
136
|
+
// raw, an answer to one could never close the other — the same two-halves-in-
|
|
137
|
+
// two-id-spaces failure that made `answered` impossible the first time.
|
|
138
|
+
const aliases = readAliases( entities )
|
|
139
|
+
const out: SpokenTurn[] = []
|
|
140
|
+
for( const [ id, e ] of entities ){
|
|
141
|
+
if( e.type !== SENT_TYPE ) continue
|
|
142
|
+
const m = meta( e )
|
|
143
|
+
const target = str( m['targetEntityId'] )
|
|
144
|
+
if( !target ) continue
|
|
145
|
+
out.push({
|
|
146
|
+
entityId: id,
|
|
147
|
+
targetEntityId: canonicalOf( aliases, target ),
|
|
148
|
+
targetEntityName: str( m['targetEntityName'] ),
|
|
149
|
+
preview: str( m['preview'] ) ?? '',
|
|
150
|
+
tick: tickOf( e, m ),
|
|
151
|
+
answeredAt: num( m['answeredAt'] ) as Tick | undefined,
|
|
152
|
+
answeredWith: str( m['answeredWith'] ),
|
|
153
|
+
unansweredAt: num( m['unansweredAt'] ) as Tick | undefined,
|
|
154
|
+
isAck: m['isAck'] === true,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
// Stable, deterministic order: oldest first, ties broken by id so two turns on
|
|
158
|
+
// one tick never swap between a run and its replay.
|
|
159
|
+
return out.sort( ( a, b ) => a.tick - b.tick || ( a.entityId < b.entityId ? -1 : a.entityId > b.entityId ? 1 : 0 ) )
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* When each person last spoke TO the mind, from `conversation.received`.
|
|
164
|
+
*
|
|
165
|
+
* This is the evidence that an utterance was answered. It is deliberately the
|
|
166
|
+
* whole of the evidence: any turn from them after we spoke counts, without trying
|
|
167
|
+
* to decide whether it was *about* what we said. Matching on topic would be a
|
|
168
|
+
* guess dressed as a fact, and getting it wrong in the strict direction would
|
|
169
|
+
* teach the mind it is being ignored by someone who is talking to it.
|
|
170
|
+
*/
|
|
171
|
+
export function lastHeardByEntity( entities: ReadonlyMap<string, EntityLike> ): Map<string, Heard> {
|
|
172
|
+
const aliases = readAliases( entities )
|
|
173
|
+
const out = new Map<string, Heard>()
|
|
174
|
+
for( const [ , e ] of entities ){
|
|
175
|
+
if( e.type !== RECEIVED_TYPE ) continue
|
|
176
|
+
const m = meta( e )
|
|
177
|
+
const raw = str( m['sourceKeid'] )
|
|
178
|
+
if( !raw ) continue
|
|
179
|
+
const source = canonicalOf( aliases, raw )
|
|
180
|
+
const at = tickOf( e, m )
|
|
181
|
+
if( at > ( out.get( source )?.tick ?? -Infinity ) )
|
|
182
|
+
out.set( source, { tick: at, preview: str( m['preview'] ) ?? '' } )
|
|
183
|
+
}
|
|
184
|
+
return out
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** When someone last spoke to us, AND what they said. */
|
|
188
|
+
export interface Heard { tick: Tick; preview: string }
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* When each person last spoke to the mind — the DURABLE version.
|
|
192
|
+
*
|
|
193
|
+
* `lastHeardByEntity` reads `conversation.received`, which lives exactly one
|
|
194
|
+
* tick: SocialPerception sweeps it as a one-shot event. So it answers "did
|
|
195
|
+
* someone speak to me THIS tick", never "when did they last speak to me".
|
|
196
|
+
*
|
|
197
|
+
* `answeredAt` is the durable half of the same fact, folded onto the mind's own
|
|
198
|
+
* `conversation.sent` turns by `resolveReplyExpectations` — it is what renders
|
|
199
|
+
* "they answered" in the prompt, and it survives snapshots.
|
|
200
|
+
*
|
|
201
|
+
* Written for the delivery-time re-check (`situationMoved`). That check first
|
|
202
|
+
* shipped keyed only on the mind having SPOKEN since composing, on the argument
|
|
203
|
+
* that nothing durable recorded the other direction. That was wrong — this is
|
|
204
|
+
* it — and the gap was the common case: live, a COO delivered a pre-composed
|
|
205
|
+
* agenda message two seconds after the person changed the subject, having said
|
|
206
|
+
* nothing in between, so the stale words arrived BEFORE her real reply and
|
|
207
|
+
* nothing was looking.
|
|
208
|
+
*/
|
|
209
|
+
export function lastAnsweredByEntity( entities: ReadonlyMap<string, EntityLike> ): Map<string, Tick> {
|
|
210
|
+
const out = new Map<string, Tick>()
|
|
211
|
+
for( const [ , e ] of entities ){
|
|
212
|
+
if( e.type !== SENT_TYPE ) continue
|
|
213
|
+
const m = meta( e )
|
|
214
|
+
const target = str( m['targetEntityId'] )
|
|
215
|
+
const at = num( m['answeredAt'] )
|
|
216
|
+
if( !target || at === undefined ) continue
|
|
217
|
+
if( at > ( out.get( target ) ?? -Infinity ) ) out.set( target, at as Tick )
|
|
218
|
+
}
|
|
219
|
+
return out
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A turn still in the air: said, not acknowledged-only, and not yet answered. */
|
|
223
|
+
export function isOpen( t: SpokenTurn ): boolean {
|
|
224
|
+
return !t.isAck && t.answeredAt === undefined
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface Resolution {
|
|
228
|
+
/** Turns the target has now answered — fold `answeredAt` + what they said into them. */
|
|
229
|
+
answered: Array<{ turn: SpokenTurn; at: Tick; with: string }>
|
|
230
|
+
/** Turns whose reply window closed in silence, newly, this tick. */
|
|
231
|
+
unanswered: SpokenTurn[]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Fold the world's answer (or its absence) onto the turns still in the air.
|
|
236
|
+
*
|
|
237
|
+
* Pure: reads frozen state, writes nothing, decides nothing about what the mind
|
|
238
|
+
* should do next. A turn resolves exactly once — `answeredAt`/`unansweredAt` are
|
|
239
|
+
* the latches, so the announcement fires on one tick rather than every tick for
|
|
240
|
+
* the rest of the session.
|
|
241
|
+
*
|
|
242
|
+
* A silence is only reported once the window has fully elapsed. Before that the
|
|
243
|
+
* turn is simply open, which is a third state and the honest one: not yet
|
|
244
|
+
* answered is not the same as ignored, and collapsing them would have the mind
|
|
245
|
+
* conclude it was being snubbed one tick after speaking.
|
|
246
|
+
*/
|
|
247
|
+
export function resolveReplyExpectations(
|
|
248
|
+
entities: ReadonlyMap<string, EntityLike>,
|
|
249
|
+
tick: Tick,
|
|
250
|
+
windowTicks: number = DEFAULT_REPLY_WINDOW_TICKS,
|
|
251
|
+
): Resolution {
|
|
252
|
+
const turns = readSpokenTurns( entities )
|
|
253
|
+
const lastHeard = lastHeardByEntity( entities )
|
|
254
|
+
|
|
255
|
+
const answered: Resolution['answered'] = []
|
|
256
|
+
const unanswered: SpokenTurn[] = []
|
|
257
|
+
|
|
258
|
+
for( const t of turns ){
|
|
259
|
+
if( t.isAck || t.answeredAt !== undefined ) continue
|
|
260
|
+
|
|
261
|
+
const heard = lastHeard.get( t.targetEntityId )
|
|
262
|
+
if( heard !== undefined && heard.tick > t.tick ){
|
|
263
|
+
answered.push({ turn: t, at: heard.tick, with: heard.preview })
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Already announced as a silence — stays open, but says nothing more.
|
|
268
|
+
if( t.unansweredAt !== undefined ) continue
|
|
269
|
+
// A turn stamped LATER than now came back with a restored snapshot whose tick
|
|
270
|
+
// counter has not caught up. Reading it as `tick - t.tick` negative would make
|
|
271
|
+
// it trivially "not yet due" forever, which is the benign direction, but a
|
|
272
|
+
// window <= 0 would then fire on everything at once. Guard both.
|
|
273
|
+
if( windowTicks > 0 && tick - t.tick >= windowTicks ) unanswered.push( t )
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return { answered, unanswered }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The turns worth showing the mind: still in the air, newest first, capped.
|
|
281
|
+
*
|
|
282
|
+
* This is what makes repetition visible from the inside. `conversation.sent` has
|
|
283
|
+
* always been in state — 57 records on the Will above — and reached no prompt at
|
|
284
|
+
* all, so the mind's only view of having spoken was `✓ reach-out` under "Recent
|
|
285
|
+
* Action Outcomes": a tick mark, no words, no person, no silence.
|
|
286
|
+
*/
|
|
287
|
+
export function openTurns(
|
|
288
|
+
entities: ReadonlyMap<string, EntityLike>,
|
|
289
|
+
limit: number = 6,
|
|
290
|
+
): SpokenTurn[] {
|
|
291
|
+
return readSpokenTurns( entities ).filter( isOpen ).reverse().slice( 0, limit )
|
|
292
|
+
}
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from '#agency/selection.scoring'
|
|
38
38
|
import { readEffectiveParams, readPersonaPrior } from '#cognition/persona.prior'
|
|
39
39
|
import { RUPTURE_REVOKE_GATE, revocationEntity } from '#agency/revocation'
|
|
40
|
+
import { liveSettlements, settlementId } from '#agency/settlement'
|
|
40
41
|
import { liveConsequences, matchConsequenceText } from '#agency/consequence'
|
|
41
42
|
import { asFinality } from '#stem/policy/arbiter'
|
|
42
43
|
|
|
@@ -285,8 +286,23 @@ export class ActionSelector implements CognitiveEngine {
|
|
|
285
286
|
catch( err ){ logger.warn(`[selector] rupture publish failed: ${ err instanceof Error ? err.message : String( err ) }`) }
|
|
286
287
|
}
|
|
287
288
|
|
|
289
|
+
// A verdict is held only while the situation that produced it stands. A hard
|
|
290
|
+
// rupture means the world just contradicted the premises the mind decided
|
|
291
|
+
// on, so every standing settlement lapses and those questions are genuinely
|
|
292
|
+
// open again — the same reasoning that revokes a commitment still being
|
|
293
|
+
// weighed, applied to the ones already settled.
|
|
294
|
+
//
|
|
295
|
+
// Deliberately NOT conditioned on something being deliberated right now: the
|
|
296
|
+
// entire point of a settlement is that it outlives the intent that formed
|
|
297
|
+
// it, so it has to be able to lapse without one.
|
|
298
|
+
const settlementDrops = rupture >= RUPTURE_REVOKE_GATE
|
|
299
|
+
? liveSettlements( state.entities, tick ).map( s => settlementId( s.schema, s.targetEntityId ) )
|
|
300
|
+
: []
|
|
301
|
+
const dropSettled = settlementDrops.length > 0 ? { delete: settlementDrops } : {}
|
|
302
|
+
|
|
288
303
|
const busy = ( n: number ): EngineResult => ({
|
|
289
304
|
commands: {
|
|
305
|
+
...dropSettled,
|
|
290
306
|
metrics: [
|
|
291
307
|
[ 'agency.field.eligible', n ],
|
|
292
308
|
[ 'agency.selection.busy', 1 ],
|
|
@@ -323,6 +339,7 @@ export class ActionSelector implements CognitiveEngine {
|
|
|
323
339
|
return {
|
|
324
340
|
commands: {
|
|
325
341
|
set: [ revocationEntity( deliberating.id, deliberating.schema, revRupture, tick ) ],
|
|
342
|
+
...dropSettled,
|
|
326
343
|
metrics: [
|
|
327
344
|
[ 'agency.field.eligible', eligible.length ],
|
|
328
345
|
[ 'agency.selection.busy', 1 ],
|
|
@@ -340,6 +357,7 @@ export class ActionSelector implements CognitiveEngine {
|
|
|
340
357
|
if( eligible.length === 0 )
|
|
341
358
|
return {
|
|
342
359
|
commands: {
|
|
360
|
+
...dropSettled,
|
|
343
361
|
metrics: [
|
|
344
362
|
[ 'agency.field.eligible', 0 ],
|
|
345
363
|
[ 'agency.selection.busy',( awaiting || composite ) ? 1 : 0 ],
|
|
@@ -371,6 +389,42 @@ export class ActionSelector implements CognitiveEngine {
|
|
|
371
389
|
const winner = scored[0]
|
|
372
390
|
if( !winner ) return busy( eligible.length )
|
|
373
391
|
|
|
392
|
+
// Whenever more than one person is a candidate for contact, record how the
|
|
393
|
+
// competition between them actually resolved. A Will that names someone and
|
|
394
|
+
// never messages them looks identical from outside whether the intention was
|
|
395
|
+
// never formed or was formed and beaten every cycle; this is the only place
|
|
396
|
+
// that difference is visible, and it is one line per contested tick.
|
|
397
|
+
// Why a WILLED contact did not happen. Gated on the executive having actually
|
|
398
|
+
// willed one (source 'ideomotor') that is not the winner — so a mind whose
|
|
399
|
+
// decision is being enacted logs nothing, and a mind that decided to contact
|
|
400
|
+
// someone and then did not says exactly what beat it.
|
|
401
|
+
//
|
|
402
|
+
// The earlier version of this gated on `reachers.length > 1`, assuming the
|
|
403
|
+
// failure was one addressee out-competing another. It is not: each master cycle
|
|
404
|
+
// sweeps every executive intent it did not name that cycle, so there is usually
|
|
405
|
+
// exactly ONE reach-out candidate in the field and the contest log never fired.
|
|
406
|
+
// The competition that matters is the willed contact against the rest of the
|
|
407
|
+
// field, which is what this prints.
|
|
408
|
+
// EVERY willed contact that lost, not just the strongest. An earlier version
|
|
409
|
+
// used `.find()`, which reports only the highest-scoring one — so with two
|
|
410
|
+
// live intents a second loser went unlogged, and the log would have quietly
|
|
411
|
+
// understated how many people the mind decided to contact and then did not.
|
|
412
|
+
const lostReaches = scored.filter( s =>
|
|
413
|
+
s !== winner
|
|
414
|
+
&& s.affordance.schema === 'reach-out'
|
|
415
|
+
&& s.affordance.source === 'ideomotor'
|
|
416
|
+
&& s.affordance.targetEntityId )
|
|
417
|
+
|
|
418
|
+
for( const lost of lostReaches )
|
|
419
|
+
logger.info(
|
|
420
|
+
`[selector] willed reach-out → ${ lost.affordance.targetEntityId } NOT selected: ` +
|
|
421
|
+
`${ lost.activation.toFixed( 3 ) }` +
|
|
422
|
+
`${ lost.affordance.justEnacted ? ` (justEnacted ${ lost.affordance.justEnacted.toFixed( 2 ) })` : '' }` +
|
|
423
|
+
` < ${ winner.affordance.schema }` +
|
|
424
|
+
`${ winner.affordance.targetEntityId ? `→${ winner.affordance.targetEntityId }` : '' }` +
|
|
425
|
+
` ${ winner.activation.toFixed( 3 ) }`
|
|
426
|
+
)
|
|
427
|
+
|
|
374
428
|
// ── Preempt a mid-composite routine (IMMEDIATE SWITCH) ────────
|
|
375
429
|
// A strong/high-stakes challenger cuts the routine off AND takes the body
|
|
376
430
|
// the same tick. We cannot delete the parent here: the executor runs later
|
|
@@ -426,12 +480,37 @@ export class ActionSelector implements CognitiveEngine {
|
|
|
426
480
|
const sameAction = winner.affordance.schema === awaiting.schema && ( winner.affordance.targetEntityId ?? '') === awaiting.target
|
|
427
481
|
if( sameAction ) return busy( eligible.length ) // field still favours what we await
|
|
428
482
|
|
|
429
|
-
|
|
483
|
+
// Clamped at BOTH ends. The upper bound was always there; the lower one matters
|
|
484
|
+
// because a restored intent's age is negative (state snapshots, the tick counter
|
|
485
|
+
// restarts at 1), and an unclamped negative staleness flips the decay into
|
|
486
|
+
// amplification: `1 - (-39 × 0.5)` = 20.6×, turning a 0.47 incumbent into 9.74 and
|
|
487
|
+
// making it permanently unpreemptable. The executor now clears such intents, so
|
|
488
|
+
// this is the second line of defence rather than the fix — but an incumbent's
|
|
489
|
+
// hysteresis must never be able to exceed its own recorded activation, whatever
|
|
490
|
+
// arithmetic feeds it.
|
|
491
|
+
const staleness = Math.min( 1, Math.max( 0, ( tick - awaiting.dispatchedAt ) / AWAIT_STALE_TICKS ) )
|
|
430
492
|
const incumbentStrength = awaiting.activation * ( 1 - staleness * STALE_DECAY )
|
|
431
493
|
const switchCost = effSwitchCost * ( 1 - stakes( winner.affordance, bias ) )
|
|
432
494
|
|
|
433
|
-
if( winner.activation <= incumbentStrength + switchCost )
|
|
495
|
+
if( winner.activation <= incumbentStrength + switchCost ){
|
|
496
|
+
// A challenger aimed at a DIFFERENT person losing to an awaiting incumbent
|
|
497
|
+
// is the starvation shape: every proactive reach-out sits 'awaiting' while a
|
|
498
|
+
// facet authors its words (8–18s = many ticks), and the incumbent's recorded
|
|
499
|
+
// activation does not decay with the challenger's damping. If one addressee
|
|
500
|
+
// can hold the channel this way, a Will can decide to contact someone else
|
|
501
|
+
// every cycle and never once do it — which is exactly the report.
|
|
502
|
+
if( winner.affordance.schema === 'reach-out'
|
|
503
|
+
&& winner.affordance.targetEntityId
|
|
504
|
+
&& winner.affordance.targetEntityId !== awaiting.target )
|
|
505
|
+
logger.info(
|
|
506
|
+
`[selector] reach-out → ${ winner.affordance.targetEntityId } BLOCKED by awaiting ` +
|
|
507
|
+
`${ awaiting.schema } → ${ awaiting.target || '—' }: ` +
|
|
508
|
+
`challenger ${ winner.activation.toFixed( 3 ) } ≤ incumbent ${ incumbentStrength.toFixed( 3 ) } ` +
|
|
509
|
+
`+ switch ${ switchCost.toFixed( 3 ) } (awaiting ${ tick - awaiting.dispatchedAt } ticks)`
|
|
510
|
+
)
|
|
511
|
+
|
|
434
512
|
return busy( eligible.length ) // not worth interrupting — keep waiting
|
|
513
|
+
}
|
|
435
514
|
|
|
436
515
|
preemptDelete = awaiting.id // PREEMPT — fall through and commit the challenger
|
|
437
516
|
preemptedFrom = awaiting.schema
|
|
@@ -478,7 +557,12 @@ export class ActionSelector implements CognitiveEngine {
|
|
|
478
557
|
targetEntityId: winner.affordance.targetEntityId,
|
|
479
558
|
parameters: winner.affordance.parameters,
|
|
480
559
|
source: winner.affordance.source,
|
|
481
|
-
//
|
|
560
|
+
// What evoked this — for an ideomotor winner, the `ideomotor.intent` entity
|
|
561
|
+
// the executive wrote. Carried so the executor can DISCHARGE it once the act
|
|
562
|
+
// happens: an intention that has been acted on is no longer an intention, and
|
|
563
|
+
// nothing was deleting these. See MotorSchemaExecutor._dischargeWill.
|
|
564
|
+
...( winner.affordance.evokedBy ? { evokedBy: winner.affordance.evokedBy } : {} ),
|
|
565
|
+
// The plan link (when a plan's frontier-step prior won the competition) —
|
|
482
566
|
// flows through the executor's action.outcome so the PlanningEngine advances.
|
|
483
567
|
...( winner.affordance.planId ? { planId: winner.affordance.planId } : {} ),
|
|
484
568
|
...( winner.affordance.stepId ? { stepId: winner.affordance.stepId } : {} ),
|
|
@@ -562,7 +646,9 @@ export class ActionSelector implements CognitiveEngine {
|
|
|
562
646
|
set: compositeTombstone
|
|
563
647
|
? [ intent, revocationEntity( compositeTombstone, compositeFrom ?? '', rupture, tick ) ]
|
|
564
648
|
: [ intent ],
|
|
565
|
-
...( preemptDelete
|
|
649
|
+
...( preemptDelete || settlementDrops.length > 0
|
|
650
|
+
? { delete: [ ...( preemptDelete ? [ preemptDelete ] : [] ), ...settlementDrops ] }
|
|
651
|
+
: {} ),
|
|
566
652
|
metrics: [
|
|
567
653
|
[ 'agency.field.eligible', eligible.length ],
|
|
568
654
|
[ 'agency.selection.busy', 0 ],
|
|
@@ -619,6 +705,14 @@ function computeRupture(
|
|
|
619
705
|
for( const e of state.entities.values() ){
|
|
620
706
|
if( e.type !== 'percept') continue
|
|
621
707
|
const m = e.metadata
|
|
708
|
+
// Deliberately NOT asProvenance(). Absence is load-bearing here and only
|
|
709
|
+
// here: an UNTAGGED percept is excluded from rupture, and three writers
|
|
710
|
+
// are untagged — `escalation.buffer` (x2) and the wake percept in
|
|
711
|
+
// `stem/index.ts`. Normalizing would default them to 'exafferent' and make
|
|
712
|
+
// them rupture-eligible. That is a real behaviour change, and probably the
|
|
713
|
+
// right one — a mind waking after hours offline currently cannot be
|
|
714
|
+
// ruptured by that fact — but it belongs to the phase that tags those
|
|
715
|
+
// writers, with tests. See .TODO/SIGNAL_BOUNDARY.md.
|
|
622
716
|
if( str( m?.['provenance'] ) !== 'exafferent') continue
|
|
623
717
|
const pTick = num( m?.['tick'], -1 )
|
|
624
718
|
if( pTick < 0 || tick - pTick > RUPTURE_WINDOW_TICKS ) continue
|
|
@@ -677,6 +771,19 @@ function effectiveWeights( state: ReadonlySimulationState ): ScoreWeights {
|
|
|
677
771
|
...DEFAULT_WEIGHTS,
|
|
678
772
|
risk: Math.max( 0, num( p['riskWeight'], DEFAULT_WEIGHTS.risk ) ),
|
|
679
773
|
novelty: Math.max( 0, num( p['noveltyWeight'], DEFAULT_WEIGHTS.novelty ) ),
|
|
774
|
+
// How long the mind sits with something it has already said before saying it
|
|
775
|
+
// again. Agreeableness raises it (does not badger); demonstrated persistence
|
|
776
|
+
// lowers it (follows up sooner). Clamped at 0 so a prior can flatten the
|
|
777
|
+
// damping into indifference but never turn repeating into a *reward*.
|
|
778
|
+
repeat: Math.max( 0, num( p['repeatDamping'], DEFAULT_WEIGHTS.repeat ) ),
|
|
779
|
+
// How much the mind's learned read on a person biases acting toward them.
|
|
780
|
+
// Deliberately NOT clamped at 0 — this weight is genuinely signed territory,
|
|
781
|
+
// and it is the tenant's to occupy. A warm, reciprocal mind leans toward
|
|
782
|
+
// whoever answers; a dogged, duty-bound one chases a silence precisely
|
|
783
|
+
// because it is silent. Both are coherent people. The container supplies the
|
|
784
|
+
// term and the persona supplies the sign; hardcoding it here would be the
|
|
785
|
+
// container deciding what kind of colleague every tenant has to be.
|
|
786
|
+
social: num( p['socialWeight'], DEFAULT_WEIGHTS.social ),
|
|
680
787
|
}
|
|
681
788
|
}
|
|
682
789
|
|
|
@@ -713,7 +820,31 @@ function buildBias( state: ReadonlySimulationState ): BiasContext {
|
|
|
713
820
|
|
|
714
821
|
// ─── entity decoding ─────────────────────────────────────────────────────────
|
|
715
822
|
|
|
716
|
-
|
|
823
|
+
/**
|
|
824
|
+
* Decode an `affordance` entity back into the thing the competition scores.
|
|
825
|
+
*
|
|
826
|
+
* This is the ONE hop between synthesis and selection, and three fields did not
|
|
827
|
+
* cross it — each the payload of a mechanism that therefore never ran:
|
|
828
|
+
*
|
|
829
|
+
* • `justEnacted` — `scoreAffordance` subtracts `w.repeat × justEnacted`, the
|
|
830
|
+
* largest damping weight in the competition (0.30). Undefined here meant ×0
|
|
831
|
+
* always, so no act has ever been damped for having just been done. EXAFFERENCE
|
|
832
|
+
* P5 and the `spokenAt` durability fix both landed on a field nothing read.
|
|
833
|
+
* • `availability` — POLICY_REAFFERENCE P2 damps a refused ability so it competes
|
|
834
|
+
* weakly instead of vanishing. `a.availability ?? 1` read the fallback forever.
|
|
835
|
+
* • `settled` — a verdict System 2 already reached. Same weight class as
|
|
836
|
+
* `justEnacted` and the same failure if dropped: the mind re-deliberates a
|
|
837
|
+
* question it has answered, forever. Added WITH its decoder rather than
|
|
838
|
+
* after someone notices the mechanism is dark.
|
|
839
|
+
* • `description` — the schema's MEANING. The selector snapshots it onto
|
|
840
|
+
* `metadata.candidates` and the DeliberationEngine renders it as `— <what>`,
|
|
841
|
+
* so the candidate list the mind reasons over listed acts with no meanings.
|
|
842
|
+
*
|
|
843
|
+
* Exported for the round-trip guard: what synthesis writes, selection must read.
|
|
844
|
+
* Four fields in this arc have been carried most of the way and lost at the last
|
|
845
|
+
* hop, so the property is pinned rather than re-checked by eye.
|
|
846
|
+
*/
|
|
847
|
+
export function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<string, unknown> | undefined ): Affordance {
|
|
717
848
|
const meta = ( m ?? {} ) as Record<string, unknown>
|
|
718
849
|
return {
|
|
719
850
|
id,
|
|
@@ -729,6 +860,12 @@ function readAffordance( id: string, m: ReadonlyMap<string, unknown> | Record<st
|
|
|
729
860
|
available: meta['available'] === true,
|
|
730
861
|
tags: Array.isArray( meta['tags'] ) ? ( meta['tags'] as unknown[] ).filter( ( t ): t is string => typeof t === 'string') : [],
|
|
731
862
|
planBias: typeof meta['planBias'] === 'number' ? ( meta['planBias'] as number ) : undefined,
|
|
863
|
+
willBias: typeof meta['willBias'] === 'number' ? ( meta['willBias'] as number ) : undefined,
|
|
864
|
+
socialPrior: typeof meta['socialPrior'] === 'number' ? ( meta['socialPrior'] as number ) : undefined,
|
|
865
|
+
justEnacted: typeof meta['justEnacted'] === 'number' ? ( meta['justEnacted'] as number ) : undefined,
|
|
866
|
+
settled: typeof meta['settled'] === 'number' ? ( meta['settled'] as number ) : undefined,
|
|
867
|
+
availability: typeof meta['availability'] === 'number' ? ( meta['availability'] as number ) : undefined,
|
|
868
|
+
description: str( meta['description'] ),
|
|
732
869
|
planId: str( meta['planId'] ),
|
|
733
870
|
stepId: str( meta['stepId'] ),
|
|
734
871
|
tick: num( meta['tick'], 0 ),
|