@mindot/will 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +87 -22
  2. package/dist/channels/discord.d.ts +67 -6
  3. package/dist/channels/discord.js +112 -6
  4. package/dist/channels/discord.js.map +1 -1
  5. package/dist/channels/whatsapp.d.ts +1 -1
  6. package/dist/channels/whatsapp.js +4 -1
  7. package/dist/channels/whatsapp.js.map +1 -1
  8. package/dist/cli.js +13733 -11057
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +3729 -1199
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/effectors.d.ts +1 -1
  14. package/dist/{will-DAW0l-lY.d.ts → will-DbDj_TEH.d.ts} +1214 -93
  15. package/package.json +1 -1
  16. package/src/channels/discord.ts +189 -11
  17. package/src/channels/types.ts +90 -0
  18. package/src/channels/whatsapp.ts +13 -4
  19. package/src/cli.ts +9 -4
  20. package/src/cognition/agency/consequence.ts +122 -1
  21. package/src/cognition/agency/conversation.aim.ts +260 -0
  22. package/src/cognition/agency/engines/action.selector.ts +85 -3
  23. package/src/cognition/agency/engines/affordance.synthesizer.ts +90 -1
  24. package/src/cognition/agency/engines/motor.schema.executor.ts +152 -10
  25. package/src/cognition/agency/engines/reafference.engine.ts +129 -2
  26. package/src/cognition/agency/proactive.communicator.ts +19 -3
  27. package/src/cognition/agency/reconcile.learning.ts +16 -2
  28. package/src/cognition/agency/restart.ts +66 -0
  29. package/src/cognition/agency/schemas/repertoire.ts +12 -5
  30. package/src/cognition/agency/selection.scoring.ts +33 -0
  31. package/src/cognition/agency/types.ts +35 -0
  32. package/src/cognition/cache/composition.ts +232 -0
  33. package/src/cognition/cache/deliberation.cache.ts +219 -0
  34. package/src/cognition/cache/fingerprint.ts +120 -0
  35. package/src/cognition/cache/types.ts +105 -0
  36. package/src/cognition/config.mirror.entities.ts +109 -1
  37. package/src/cognition/event.schemas.ts +22 -0
  38. package/src/cognition/faculties/autobiographical.narrator.ts +5 -10
  39. package/src/cognition/faculties/episodic.consolidator.ts +59 -3
  40. package/src/cognition/faculties/executive.engine/commands.ts +189 -14
  41. package/src/cognition/faculties/executive.engine/context.ts +67 -13
  42. package/src/cognition/faculties/executive.engine/deliberate.reasoning.ts +1 -1
  43. package/src/cognition/faculties/executive.engine/engine.ts +686 -187
  44. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +162 -44
  45. package/src/cognition/faculties/executive.engine/facet.supervisor.ts +310 -65
  46. package/src/cognition/faculties/executive.engine/facet.ts +89 -26
  47. package/src/cognition/faculties/executive.engine/gating.ts +14 -14
  48. package/src/cognition/faculties/executive.engine/parser.ts +21 -1
  49. package/src/cognition/faculties/executive.engine/prompt.factory.ts +169 -20
  50. package/src/cognition/faculties/executive.engine/types.ts +69 -0
  51. package/src/cognition/faculties/goal.manager.ts +94 -14
  52. package/src/cognition/faculties/known.entity.tracker.ts +267 -28
  53. package/src/cognition/faculties/moral.evaluator.ts +8 -3
  54. package/src/cognition/faculties/persona.consolidator.ts +141 -0
  55. package/src/cognition/faculties/reputation.tracker.ts +66 -2
  56. package/src/cognition/faculties/self.model.updater.ts +19 -12
  57. package/src/cognition/faculties/social.perception.ts +47 -3
  58. package/src/cognition/faculties/threat.evaluator.ts +7 -0
  59. package/src/cognition/faculties/working.memory.ts +10 -20
  60. package/src/cognition/identity.entity.ts +205 -0
  61. package/src/cognition/index.ts +11 -0
  62. package/src/cognition/memory/vector.adapter.ts +12 -3
  63. package/src/cognition/memory/vector.embedder.ts +54 -7
  64. package/src/cognition/persona.prior.ts +6 -0
  65. package/src/cognition/senses/audition.engine/engine.ts +404 -46
  66. package/src/cognition/senses/base.sense.engine.ts +1 -1
  67. package/src/cognition/senses/index.ts +12 -0
  68. package/src/cognition/social.identity.ts +273 -0
  69. package/src/cognition/utilities/token.tracker.ts +246 -98
  70. package/src/core/orchestrator.ts +38 -0
  71. package/src/host/boot.ts +78 -22
  72. package/src/index.ts +35 -0
  73. package/src/llm/index.ts +415 -97
  74. package/src/llm/routing.ts +204 -0
  75. package/src/llm/summarizer.ts +5 -1
  76. package/src/llm/wire.contracts.ts +19 -0
  77. package/src/pma/index.ts +67 -53
  78. package/src/runners/thin-shim.runner.ts +18 -6
  79. package/src/sdk/will.ts +121 -22
  80. package/src/stem/assembly.audit.ts +1 -0
  81. package/src/stem/guards/identity.coherence.ts +17 -6
  82. package/src/stem/index.ts +82 -5
  83. package/src/stem/mind.ts +327 -79
  84. package/src/stem/policy/arbiter.ts +49 -14
  85. package/src/stem/policy/rule.table.ts +2 -2
  86. package/src/stem/tracts/effector.controller.ts +56 -9
  87. package/src/stem/tracts/outbox.writer.ts +40 -2
  88. package/src/cognition/faculties/executive.engine/messages.ts +0 -102
@@ -140,6 +140,29 @@ export interface Affordance {
140
140
  * competition WITHOUT bypassing it (the plan biases; the field still decides).
141
141
  */
142
142
  planBias?: number
143
+ /**
144
+ * Top-down volitional bias 0..1 — set when the executive DELIBERATELY willed this
145
+ * affordance (source 'ideomotor'), carrying the confidence it decided with. Like
146
+ * planBias it lifts activation without bypassing the competition: a willed act must
147
+ * still out-compete the field, but it no longer arrives with the same standing as an
148
+ * ambient possibility. Without this term the ideomotor leg created candidates the
149
+ * arithmetic could not tell apart from idle ones, and a cheap unconditional reflex
150
+ * ('express', cost 0.02) beat a deliberate 'reach-out' 176 times out of 176.
151
+ */
152
+ willBias?: number
153
+ /**
154
+ * Learned social standing of this affordance's addressee, −1..1 (0 = neutral or
155
+ * unknown). Set only for an act aimed at someone. Every input is LEARNED, none
156
+ * hardcoded: ReputationTracker's trustworthiness — which is fed by
157
+ * `interaction.occurred` and so only became live once inbound conversation reached
158
+ * social cognition (#113) — weighted by that model's own confidence, plus the mind's
159
+ * current affective tone as a gentle tilt on reaching out at all.
160
+ *
161
+ * This is what makes "they never answer me" reach the competition: it arrives as a
162
+ * fading opinion of a person the mind formed itself and can revise, not as a damping
163
+ * curve applied behind its back.
164
+ */
165
+ socialPrior?: number
143
166
  /**
144
167
  * Policy availability 0..1 (POLICY_REAFFERENCE P2) — learned from refusals,
145
168
  * distinct from `available` (precondition satisfaction) and from competence.
@@ -149,6 +172,18 @@ export interface Affordance {
149
172
  * gets an occasional re-probe and can climb back as availability recovers.
150
173
  */
151
174
  availability?: number
175
+ /**
176
+ * How much of this act's OWN footprint is still live, 1 → 0 (EXAFFERENCE P5).
177
+ * Set when a consequence descriptor for the same (schema, target) has not yet
178
+ * expired — the mind has just done this and does not yet know how it landed.
179
+ *
180
+ * Damps the pull to do it again, and decays back to 0 on its own, so a silence
181
+ * that starts to matter can still out-compete it. Without this the executive's
182
+ * standing `ideomotor.intent` outlived the act it produced: the same two
183
+ * messages were delivered to the same person three times, ~21 ticks apart,
184
+ * each time as though it were the first.
185
+ */
186
+ justEnacted?: number
152
187
  /** Provenance: the plan whose frontier step projected this affordance. */
153
188
  planId?: string
154
189
  /** Provenance: the frontier step id — flows through to action.outcome so the plan advances. */
@@ -0,0 +1,232 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/cache/composition.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * Compositional operators for cache interpolation.
7
+ *
8
+ * Design principle (from the research sketch): interpolate what is safe to
9
+ * interpolate, copy verbatim what is not. Executive actions carry free-text
10
+ * fields (`reasoning`, `expectedOutcome`) that would turn to gibberish under
11
+ * blending, so the ACTIONS block uses a weighted *vote* over action type and
12
+ * then copies the winning neighbor's action objects verbatim. Numeric scalars
13
+ * (goal priority, belief confidence) are the only fields we mean-blend.
14
+ *
15
+ * Every operator is deterministic: inputs arrive pre-sorted from the cache and
16
+ * ties are resolved by neighbor order (already deterministic), never by hashing.
17
+ */
18
+
19
+ import type { ExecutiveOutputFull } from '#faculties/executive.engine/types'
20
+ import type { ScoredNeighbor, CacheScope } from './types'
21
+
22
+ /**
23
+ * Compose a valid ExecutiveOutputFull from scored neighbors. `neighbors` must be
24
+ * non-empty and ordered best-first (as the cache returns them). The three
25
+ * required fields (actions, reasoning, confidence) are always populated.
26
+ */
27
+ export function composeOutput(
28
+ neighbors: ScoredNeighbor[],
29
+ tau: number,
30
+ scopes: CacheScope[],
31
+ ): ExecutiveOutputFull {
32
+ const weights = _softmaxWeights( neighbors, tau )
33
+
34
+ // The best neighbor anchors required fields when a scope leaves them empty.
35
+ const anchor = neighbors[ 0 ]!.pattern.output
36
+
37
+ const out: ExecutiveOutputFull = {
38
+ actions: [],
39
+ reasoning: anchor.reasoning ?? '',
40
+ confidence: anchor.confidence ?? 0.5,
41
+ }
42
+
43
+ if( scopes.includes('actions') ){
44
+ const composed = _composeActions( neighbors, weights )
45
+ out.actions = composed.actions
46
+ out.reasoning = composed.reasoning
47
+ out.confidence = composed.confidence
48
+ } else {
49
+ // Actions are required downstream — fall back to the anchor's verbatim actions.
50
+ out.actions = _clone( anchor.actions ?? [] )
51
+ }
52
+
53
+ if( scopes.includes('goals') )
54
+ out.newGoals = _composeGoals( neighbors, weights )
55
+
56
+ if( scopes.includes('beliefs') )
57
+ out.newBeliefs = _composeBeliefs( neighbors, weights )
58
+
59
+ return out
60
+ }
61
+
62
+ // ── Weight computation ─────────────────────────────────────
63
+
64
+ function _softmaxWeights( neighbors: ScoredNeighbor[], tau: number ): number[] {
65
+ const t = tau > 0 ? tau : 1e-6
66
+ const sims = neighbors.map( n => n.similarity )
67
+ const maxSim = Math.max( ...sims )
68
+ const exps = sims.map( s => Math.exp( ( s - maxSim ) / t ) ) // shift for stability
69
+ const sum = exps.reduce( ( a, b ) => a + b, 0 )
70
+ return exps.map( e => ( sum === 0 ? 1 / exps.length : e / sum ) )
71
+ }
72
+
73
+ // ── ACTIONS: weighted type vote + verbatim copy ────────────
74
+
75
+ function _composeActions(
76
+ neighbors: ScoredNeighbor[],
77
+ weights: number[],
78
+ ): { actions: ExecutiveOutputFull['actions']; reasoning: string; confidence: number } {
79
+ // Vote on each neighbor's primary (first) action type.
80
+ const typeWeight = new Map<string, number>()
81
+ for( let i = 0; i < neighbors.length; i++ ){
82
+ const acts = neighbors[ i ]!.pattern.output.actions ?? []
83
+ const primary = acts[ 0 ]?.type
84
+ if( primary === undefined ) continue
85
+ typeWeight.set( primary, ( typeWeight.get( primary ) ?? 0 ) + ( weights[ i ] ?? 0 ) )
86
+ }
87
+
88
+ // Winning type — deterministic: highest weight, ties broken by neighbor order
89
+ // (the first neighbor to reach the max keeps it, and neighbors are best-first).
90
+ let winningType: string | null = null
91
+ let bestWeight = -1
92
+ for( let i = 0; i < neighbors.length; i++ ){
93
+ const primary = neighbors[ i ]!.pattern.output.actions?.[ 0 ]?.type
94
+ if( primary === undefined ) continue
95
+ const w = typeWeight.get( primary ) ?? 0
96
+ if( w > bestWeight ){ bestWeight = w; winningType = primary }
97
+ }
98
+
99
+ // Copy the full action list from the highest-weight neighbor whose primary
100
+ // action type matches the winner — verbatim, so text fields stay coherent.
101
+ let source = neighbors[ 0 ]!
102
+ for( let i = 0; i < neighbors.length; i++ ){
103
+ if( neighbors[ i ]!.pattern.output.actions?.[ 0 ]?.type === winningType ){
104
+ source = neighbors[ i ]!
105
+ break
106
+ }
107
+ }
108
+
109
+ const src = source.pattern.output
110
+ return {
111
+ actions: _clone( src.actions ?? [] ),
112
+ reasoning: src.reasoning ?? '',
113
+ confidence: src.confidence ?? 0.5,
114
+ }
115
+ }
116
+
117
+ // ── GOALS: merge by description, mean priority (Phase 2) ────
118
+
119
+ function _composeGoals(
120
+ neighbors: ScoredNeighbor[],
121
+ weights: number[],
122
+ ): ExecutiveOutputFull['newGoals'] {
123
+ type Agg = {
124
+ weight: number
125
+ priority: number
126
+ tags: string[]
127
+ completionType: string
128
+ completionCondition?: string
129
+ bestW: number
130
+ }
131
+ const byDesc = new Map<string, Agg>()
132
+
133
+ for( let i = 0; i < neighbors.length; i++ ){
134
+ const w = weights[ i ] ?? 0
135
+ for( const g of neighbors[ i ]!.pattern.output.newGoals ?? [] ){
136
+ const cur = byDesc.get( g.description )
137
+ if( cur ){
138
+ cur.weight += w
139
+ cur.priority += g.priority * w
140
+ if( w > cur.bestW ){
141
+ cur.bestW = w
142
+ cur.tags = g.tags
143
+ cur.completionType = g.completionType
144
+ cur.completionCondition = g.completionCondition
145
+ }
146
+ } else {
147
+ byDesc.set( g.description, {
148
+ weight: w,
149
+ priority: g.priority * w,
150
+ tags: g.tags,
151
+ completionType: g.completionType,
152
+ completionCondition: g.completionCondition,
153
+ bestW: w,
154
+ } )
155
+ }
156
+ }
157
+ }
158
+
159
+ const result: NonNullable<ExecutiveOutputFull['newGoals']> = []
160
+ for( const [ description, a ] of byDesc ){
161
+ result.push( {
162
+ description,
163
+ priority: a.weight === 0 ? 0 : a.priority / a.weight,
164
+ tags: [ ...a.tags ],
165
+ completionType: a.completionType,
166
+ ...( a.completionCondition !== undefined ? { completionCondition: a.completionCondition } : {} ),
167
+ } )
168
+ }
169
+ result.sort( ( x, y ) => y.priority - x.priority )
170
+ return result.slice( 0, 3 )
171
+ }
172
+
173
+ // ── BELIEFS: merge by statement, mean confidence (Phase 3) ─
174
+
175
+ function _composeBeliefs(
176
+ neighbors: ScoredNeighbor[],
177
+ weights: number[],
178
+ ): ExecutiveOutputFull['newBeliefs'] {
179
+ type Agg = {
180
+ weight: number
181
+ confidence: number
182
+ category: string
183
+ evidence: 'single_observation' | 'recurring_pattern' | 'strong_pattern'
184
+ tags: string[]
185
+ bestW: number
186
+ }
187
+ const byStmt = new Map<string, Agg>()
188
+
189
+ for( let i = 0; i < neighbors.length; i++ ){
190
+ const w = weights[ i ] ?? 0
191
+ for( const b of neighbors[ i ]!.pattern.output.newBeliefs ?? [] ){
192
+ const cur = byStmt.get( b.statement )
193
+ if( cur ){
194
+ cur.weight += w
195
+ cur.confidence += b.confidence * w
196
+ if( w > cur.bestW ){
197
+ cur.bestW = w
198
+ cur.category = b.category
199
+ cur.evidence = b.evidence
200
+ cur.tags = b.tags
201
+ }
202
+ } else {
203
+ byStmt.set( b.statement, {
204
+ weight: w,
205
+ confidence: b.confidence * w,
206
+ category: b.category,
207
+ evidence: b.evidence,
208
+ tags: b.tags,
209
+ bestW: w,
210
+ } )
211
+ }
212
+ }
213
+ }
214
+
215
+ const result: NonNullable<ExecutiveOutputFull['newBeliefs']> = []
216
+ for( const [ statement, a ] of byStmt ){
217
+ result.push( {
218
+ statement,
219
+ category: a.category,
220
+ confidence: a.weight === 0 ? 0 : a.confidence / a.weight,
221
+ evidence: a.evidence,
222
+ tags: [ ...a.tags ],
223
+ } )
224
+ }
225
+ return result
226
+ }
227
+
228
+ // ── Deterministic deep clone for verbatim-copied blocks ────
229
+
230
+ function _clone<T>( v: T ): T {
231
+ return JSON.parse( JSON.stringify( v ) ) as T
232
+ }
@@ -0,0 +1,219 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/cache/deliberation.cache.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * DeliberationCache — deterministic fast-path for executive reasoning.
7
+ *
8
+ * Stores past (fingerprint, output) pairs and composes a new output by
9
+ * interpolating neighbors of the *query* fingerprint. Pure, R2-safe, bounded.
10
+ *
11
+ * Determinism discipline:
12
+ * - Patterns live in a flat array; retrieval is a linear scan (k is small and
13
+ * the array is bounded, so determinism beats micro-optimisation).
14
+ * - The similarity that drives confidence, weighting and competence updates is
15
+ * always computed against the query fingerprint the caller passes in — never
16
+ * a self-similarity proxy.
17
+ * - Sort keys are scalars; ties break on storedAtTick (older first).
18
+ * - No hash maps over fingerprints, no Set iteration, no wall-clock, no RNG.
19
+ */
20
+
21
+ import type { ExecutiveOutputFull } from '#faculties/executive.engine/types'
22
+ import type { Tick } from '#core/types'
23
+ import type {
24
+ DeliberationCacheConfig,
25
+ CachePattern,
26
+ CacheResult,
27
+ ScoredNeighbor,
28
+ DeliberationCacheSnapshot,
29
+ } from './types'
30
+ import { FINGERPRINT_VERSION, fingerprintSimilarity } from './fingerprint'
31
+ import { composeOutput } from './composition'
32
+
33
+ const DEFAULT_CONFIG: Required<DeliberationCacheConfig> = {
34
+ maxPatterns: 5000,
35
+ k: 5,
36
+ minSimilarity: 0.75,
37
+ theta: 0.70,
38
+ tau: 0.5,
39
+ eta: 0.1,
40
+ decayPerCycle: 0.999,
41
+ verifyEveryNHits: 5,
42
+ scopes: [ 'actions' ],
43
+ }
44
+
45
+ export class DeliberationCache {
46
+ readonly name = 'deliberation-cache'
47
+
48
+ private _patterns: CachePattern[] = []
49
+ private _config: Required<DeliberationCacheConfig>
50
+ private _hitCount = 0
51
+ private _missCount = 0
52
+ private _verifyCounter = 0
53
+
54
+ constructor( config: DeliberationCacheConfig = {} ){
55
+ this._config = { ...DEFAULT_CONFIG, ...config }
56
+ }
57
+
58
+ get size(): number { return this._patterns.length }
59
+ get hitCount(): number { return this._hitCount }
60
+ get missCount(): number { return this._missCount }
61
+
62
+ // ── Retrieval + composition ──────────────────────────────
63
+
64
+ /**
65
+ * Retrieve neighbors of `queryFp` and, if confident, compose an output.
66
+ * Confidence ρ = max over neighbors of (competence × similarity), per the
67
+ * research sketch §2.2 — a diffuse cloud of weak matches never triggers a hit.
68
+ */
69
+ retrieve( queryFp: Float32Array, _tick: Tick ): CacheResult {
70
+ const neighbors = this._retrieveNeighbors( queryFp )
71
+ if( neighbors.length === 0 ){
72
+ this._missCount++
73
+ return { output: null, confidence: 0, neighbors: [], hit: false }
74
+ }
75
+
76
+ let confidence = 0
77
+ for( const n of neighbors ){
78
+ const score = n.pattern.competence * n.similarity
79
+ if( score > confidence ) confidence = score
80
+ }
81
+
82
+ const hit = confidence >= this._config.theta
83
+ if( hit ) this._hitCount++
84
+ else this._missCount++
85
+
86
+ const output = hit
87
+ ? composeOutput( neighbors, this._config.tau, this._config.scopes )
88
+ : null
89
+
90
+ return { output, confidence, neighbors, hit }
91
+ }
92
+
93
+ /** Store a new (fingerprint, output) pair from the slow (LLM) path. */
94
+ learn( queryFp: Float32Array, output: ExecutiveOutputFull, tick: Tick ): void {
95
+ this._evictIfFull( tick )
96
+ this._patterns.push( {
97
+ fingerprint: new Float32Array( queryFp ),
98
+ output,
99
+ competence: 0.5,
100
+ storedAtTick: tick,
101
+ retrievalCount: 0,
102
+ successCount: 0,
103
+ } )
104
+ }
105
+
106
+ /**
107
+ * Update the competence of the pattern nearest to `queryFp`, from a reafference
108
+ * reward in [0,1]. Called after an action outcome is confirmed.
109
+ */
110
+ updateCompetence( queryFp: Float32Array, reward: number, _tick: Tick ): void {
111
+ const best = this._findBestMatch( queryFp )
112
+ if( !best ) return
113
+ const r = Math.max( 0, Math.min( 1, reward ) )
114
+ best.retrievalCount++
115
+ if( r > 0.5 ) best.successCount++
116
+ const a = this._config.eta
117
+ best.competence = Math.max( 0, Math.min( 1, best.competence * ( 1 - a ) + r * a ) )
118
+ }
119
+
120
+ /** Decay all competences one executive cycle. Slowly forgets stale patterns. */
121
+ decay(): void {
122
+ const f = this._config.decayPerCycle
123
+ if( f >= 1 ) return
124
+ for( const p of this._patterns ) p.competence *= f
125
+ }
126
+
127
+ /** Deterministic 1-in-N verify schedule. Increments a counter each call. */
128
+ shouldVerify(): boolean {
129
+ if( this._config.verifyEveryNHits <= 0 ) return false
130
+ this._verifyCounter++
131
+ return this._verifyCounter % this._config.verifyEveryNHits === 0
132
+ }
133
+
134
+ // ── Snapshot / restore (entity persistence + tests) ──────
135
+
136
+ snapshot(): DeliberationCacheSnapshot {
137
+ return {
138
+ version: FINGERPRINT_VERSION,
139
+ patterns: this._patterns.map( p => ( {
140
+ fingerprint: Array.from( p.fingerprint ),
141
+ output: p.output,
142
+ competence: p.competence,
143
+ storedAtTick: p.storedAtTick,
144
+ retrievalCount: p.retrievalCount,
145
+ successCount: p.successCount,
146
+ } ) ),
147
+ hitCount: this._hitCount,
148
+ missCount: this._missCount,
149
+ verifyCounter: this._verifyCounter,
150
+ }
151
+ }
152
+
153
+ restore( snap: DeliberationCacheSnapshot ): void {
154
+ // Version guard: a fingerprint layout change invalidates stored vectors.
155
+ if( snap.version !== FINGERPRINT_VERSION ) return
156
+ this._patterns = snap.patterns.map( p => ( {
157
+ fingerprint: new Float32Array( p.fingerprint ),
158
+ output: p.output,
159
+ competence: p.competence,
160
+ storedAtTick: p.storedAtTick,
161
+ retrievalCount: p.retrievalCount,
162
+ successCount: p.successCount,
163
+ } ) )
164
+ this._hitCount = snap.hitCount ?? 0
165
+ this._missCount = snap.missCount ?? 0
166
+ this._verifyCounter = snap.verifyCounter ?? 0
167
+ }
168
+
169
+ // ── Internal ─────────────────────────────────────────────
170
+
171
+ private _retrieveNeighbors( queryFp: Float32Array ): ScoredNeighbor[] {
172
+ const scored: ScoredNeighbor[] = []
173
+ for( const p of this._patterns ){
174
+ const similarity = fingerprintSimilarity( queryFp, p.fingerprint )
175
+ if( similarity >= this._config.minSimilarity )
176
+ scored.push( { pattern: p, similarity } )
177
+ }
178
+
179
+ // Rank by (similarity × competence) desc; ties → older pattern first.
180
+ scored.sort( ( a, b ) => {
181
+ const sa = a.similarity * a.pattern.competence
182
+ const sb = b.similarity * b.pattern.competence
183
+ if( sa !== sb ) return sb - sa
184
+ return a.pattern.storedAtTick - b.pattern.storedAtTick
185
+ } )
186
+
187
+ return scored.slice( 0, this._config.k )
188
+ }
189
+
190
+ private _findBestMatch( queryFp: Float32Array ): CachePattern | null {
191
+ let best: CachePattern | null = null
192
+ let bestSim = -1
193
+ for( const p of this._patterns ){
194
+ const sim = fingerprintSimilarity( queryFp, p.fingerprint )
195
+ if( sim > bestSim ){ bestSim = sim; best = p }
196
+ }
197
+ return best
198
+ }
199
+
200
+ private _evictIfFull( _tick: Tick ): void {
201
+ if( this._patterns.length < this._config.maxPatterns ) return
202
+ // Evict the LEAST competent pattern; ties break on the oldest (lowest
203
+ // storedAtTick). Age is handled by decay() — stale patterns bleed competence
204
+ // toward 0 each cycle, so competence alone already ranks them lowest. Folding
205
+ // recency INTO the score inverts value: a high-competence *old* pattern would
206
+ // score below a low-competence *new* one and be wrongly evicted.
207
+ let evictIdx = 0
208
+ let best = this._patterns[ 0 ]!
209
+ for( let i = 1; i < this._patterns.length; i++ ){
210
+ const p = this._patterns[ i ]!
211
+ if( p.competence < best.competence ||
212
+ ( p.competence === best.competence && p.storedAtTick < best.storedAtTick ) ){
213
+ best = p
214
+ evictIdx = i
215
+ }
216
+ }
217
+ this._patterns.splice( evictIdx, 1 )
218
+ }
219
+ }
@@ -0,0 +1,120 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/cognition/cache/fingerprint.ts
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ /**
6
+ * CognitiveFingerprint — extracts a deterministic scalar vector from the
7
+ * frozen simulation state. No wall-clock, no RNG, no iteration-order
8
+ * dependence (values are sorted before they enter fixed slots). The vector
9
+ * captures the operating context that most strongly conditions executive
10
+ * reasoning — the same scalars the prompt's `## Current State` block renders.
11
+ *
12
+ * Dimension layout (fixed order — never reorder without bumping the version):
13
+ * 0–2 physiology energy.level, sleep.pressure, stress.load (metrics, 0–100 → 0–1)
14
+ * 3–5 PAD affect valence (−1..1 → 0..1), arousal, dominance (metrics, 0–1)
15
+ * 6–15 goal priorities top 10 `goal` entities by metadata.priority (0..1)
16
+ * 16–25 belief confidences top 10 `belief` entities by metadata.confidence (0..1)
17
+ * 26–35 wm activations top 10 `working_memory.item` by metadata.activation (0..1)
18
+ *
19
+ * Total: 36 dimensions. Verified against the live writers:
20
+ * - interoception.ts writes energy.level / sleep.pressure (0–100)
21
+ * - stress.regulator.ts writes stress.load
22
+ * - affective.blender.ts writes affect.valence (−1..1) / arousal / dominance (0..1)
23
+ * - goal.manager.ts persists goal entities with metadata.priority
24
+ * - executive.engine/commands.ts + semantic integrator persist belief entities
25
+ * with metadata.confidence
26
+ * - working.memory.ts persists working_memory.item entities with metadata.activation
27
+ */
28
+
29
+ import type { ReadonlySimulationState } from '#core/types'
30
+
31
+ export const FINGERPRINT_DIM = 36
32
+ export const FINGERPRINT_VERSION = 1
33
+
34
+ export function extractFingerprint( state: ReadonlySimulationState ): Float32Array {
35
+ const vec = new Float32Array( FINGERPRINT_DIM )
36
+ let idx = 0
37
+
38
+ // ── Physiology (metrics are 0–100) ───────────────────────
39
+ vec[ idx++ ] = _norm( _metric( state, 'energy.level', 50 ), 0, 100 )
40
+ vec[ idx++ ] = _norm( _metric( state, 'sleep.pressure', 0 ), 0, 100 )
41
+ vec[ idx++ ] = _norm( _metric( state, 'stress.load', 0 ), 0, 100 )
42
+
43
+ // ── PAD affect ───────────────────────────────────────────
44
+ // valence is written on [-1, 1]; map to [0, 1] so a distressed state is not
45
+ // silently clamped to neutral. arousal / dominance are already [0, 1].
46
+ vec[ idx++ ] = _clamp01( ( _metric( state, 'affect.valence', 0 ) + 1 ) / 2 )
47
+ vec[ idx++ ] = _clamp01( _metric( state, 'affect.arousal', 0.3 ) )
48
+ vec[ idx++ ] = _clamp01( _metric( state, 'affect.dominance', 0.5 ) )
49
+
50
+ // ── Top-10 goal priorities ───────────────────────────────
51
+ idx = _topEntityScalars( state, 'goal', 'priority', 0, vec, idx, 10 )
52
+
53
+ // ── Top-10 belief confidences ────────────────────────────
54
+ idx = _topEntityScalars( state, 'belief', 'confidence', 0.5, vec, idx, 10 )
55
+
56
+ // ── Top-10 working-memory activations ────────────────────
57
+ idx = _topEntityScalars( state, 'working_memory.item', 'activation', 0, vec, idx, 10 )
58
+
59
+ // Defensive: zero-fill any remaining slots (never expected).
60
+ while( idx < FINGERPRINT_DIM ) vec[ idx++ ] = 0
61
+
62
+ return vec
63
+ }
64
+
65
+ /**
66
+ * Collect a metadata scalar from every entity of `type`, sort descending,
67
+ * and write the top `count` (clamped to [0,1]) into `vec` starting at `idx`.
68
+ * Sorting makes the slice order deterministic regardless of Map iteration.
69
+ * Returns the next free index.
70
+ */
71
+ function _topEntityScalars(
72
+ state: ReadonlySimulationState,
73
+ type: string,
74
+ field: string,
75
+ fallback: number,
76
+ vec: Float32Array,
77
+ idx: number,
78
+ count: number,
79
+ ): number {
80
+ const vals: number[] = []
81
+ for( const e of state.entities.values() ){
82
+ if( e.type !== type ) continue
83
+ const raw = e.metadata?.[ field ]
84
+ vals.push( typeof raw === 'number' ? raw : fallback )
85
+ }
86
+ vals.sort( ( a, b ) => b - a )
87
+ for( let i = 0; i < count; i++ )
88
+ vec[ idx++ ] = _clamp01( vals[ i ] ?? 0 )
89
+ return idx
90
+ }
91
+
92
+ function _metric( state: ReadonlySimulationState, key: string, fallback: number ): number {
93
+ const v = state.metrics.get( key )
94
+ return typeof v === 'number' ? v : fallback
95
+ }
96
+
97
+ /** Normalise a value from [min,max] → [0,1]. */
98
+ function _norm( v: number, min: number, max: number ): number {
99
+ if( max === min ) return 0.5
100
+ return Math.max( 0, Math.min( 1, ( v - min ) / ( max - min ) ) )
101
+ }
102
+
103
+ function _clamp01( v: number ): number {
104
+ if( Number.isNaN( v ) ) return 0
105
+ return Math.max( 0, Math.min( 1, v ) )
106
+ }
107
+
108
+ /** Cosine similarity between two fingerprint vectors. */
109
+ export function fingerprintSimilarity( a: Float32Array, b: Float32Array ): number {
110
+ let dot = 0, na = 0, nb = 0
111
+ for( let i = 0; i < FINGERPRINT_DIM; i++ ){
112
+ const ai = a[ i ] ?? 0
113
+ const bi = b[ i ] ?? 0
114
+ dot += ai * bi
115
+ na += ai * ai
116
+ nb += bi * bi
117
+ }
118
+ const denom = Math.sqrt( na ) * Math.sqrt( nb )
119
+ return denom === 0 ? 0 : dot / denom
120
+ }