audrey 0.8.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Audrey
2
2
 
3
- Biological memory architecture for AI agents. Gives agents cognitive memory that decays, consolidates, self-validates, and learns from experience — not just a database.
3
+ Biological memory architecture for AI agents. Memory that decays, consolidates, feels, and learns — not just a database.
4
4
 
5
5
  ## Why Audrey Exists
6
6
 
@@ -19,7 +19,7 @@ Audrey fixes all of this by modeling memory the way the brain does:
19
19
  | Neocortex | Semantic Memory | Consolidated principles and patterns |
20
20
  | Sleep Replay | Consolidation Engine | Extracts patterns from episodes, promotes to principles |
21
21
  | Prefrontal Cortex | Validation Engine | Truth-checking, contradiction detection |
22
- | Amygdala | Salience Scorer | Importance weighting for retention priority |
22
+ | Amygdala | Affect System | Emotional encoding, arousal-salience coupling, mood-congruent recall |
23
23
 
24
24
  ## Install
25
25
 
@@ -67,22 +67,26 @@ const brain = new Audrey({
67
67
  embedding: { provider: 'mock', dimensions: 8 }, // or 'openai' for production
68
68
  });
69
69
 
70
- // 2. Encode observations
70
+ // 2. Encode observations — with optional emotional context
71
71
  await brain.encode({
72
72
  content: 'Stripe API returns 429 above 100 req/s',
73
73
  source: 'direct-observation',
74
74
  tags: ['stripe', 'rate-limit'],
75
+ affect: { valence: -0.4, arousal: 0.7, label: 'frustration' },
75
76
  });
76
77
 
77
- // 3. Recall what you know
78
- const memories = await brain.recall('stripe rate limits', { limit: 5 });
79
- // Returns: [{ content, type, confidence, score, ... }]
78
+ // 3. Recall what you know — mood-congruent retrieval
79
+ const memories = await brain.recall('stripe rate limits', {
80
+ limit: 5,
81
+ mood: { valence: -0.3 }, // frustrated right now? memories encoded in frustration surface first
82
+ });
80
83
 
81
84
  // 4. Filtered recall — by tag, source, or date range
82
85
  const recent = await brain.recall('stripe', {
83
86
  tags: ['rate-limit'],
84
87
  sources: ['direct-observation'],
85
88
  after: '2026-02-01T00:00:00Z',
89
+ context: { task: 'debugging', domain: 'payments' }, // context-dependent retrieval
86
90
  });
87
91
 
88
92
  // 5. Consolidate episodes into principles (the "sleep" cycle)
@@ -127,6 +131,31 @@ const brain = new Audrey({
127
131
  minEpisodes: 3, // Minimum cluster size for principle extraction
128
132
  },
129
133
 
134
+ // Context-dependent retrieval (v0.8.0)
135
+ context: {
136
+ enabled: true, // Enable encoding-specificity principle
137
+ weight: 0.3, // Max 30% confidence boost on full context match
138
+ },
139
+
140
+ // Emotional memory (v0.9.0)
141
+ affect: {
142
+ enabled: true, // Enable affect system
143
+ weight: 0.2, // Max 20% mood-congruence boost
144
+ arousalWeight: 0.3, // Yerkes-Dodson arousal-salience coupling
145
+ resonance: { // Detect emotional echoes across experiences
146
+ enabled: true,
147
+ k: 5, // How many past episodes to check
148
+ threshold: 0.5, // Semantic similarity threshold
149
+ affectThreshold: 0.6, // Emotional similarity threshold
150
+ },
151
+ },
152
+
153
+ // Interference-based forgetting (v0.7.0)
154
+ interference: {
155
+ enabled: true, // New episodes suppress similar existing memories
156
+ weight: 0.15, // Suppression strength
157
+ },
158
+
130
159
  // Decay settings
131
160
  decay: {
132
161
  dormantThreshold: 0.1, // Below this confidence = dormant
@@ -284,6 +313,12 @@ const id = await brain.encode({
284
313
  },
285
314
  tags: ['stripe', 'production'], // Optional. Array of strings.
286
315
  supersedes: 'previous-id', // Optional. ID of episode this corrects.
316
+ context: { task: 'debugging' }, // Optional. Situational context for retrieval.
317
+ affect: { // Optional. Emotional context.
318
+ valence: -0.5, // -1 (negative) to 1 (positive)
319
+ arousal: 0.7, // 0 (calm) to 1 (activated)
320
+ label: 'frustration', // Human-readable emotion label
321
+ },
287
322
  });
288
323
  ```
289
324
 
@@ -316,6 +351,8 @@ const memories = await brain.recall('stripe rate limits', {
316
351
  sources: ['direct-observation'], // Only episodic memories from these sources
317
352
  after: '2026-02-01T00:00:00Z', // Only memories created after this date
318
353
  before: '2026-03-01T00:00:00Z', // Only memories created before this date
354
+ context: { task: 'debugging' }, // Boost memories encoded in matching context
355
+ mood: { valence: -0.3, arousal: 0.5 }, // Mood-congruent retrieval
319
356
  });
320
357
  ```
321
358
 
@@ -332,6 +369,8 @@ Each result:
332
369
  score: 0.74, // similarity * confidence
333
370
  source: 'consolidation',
334
371
  state: 'active',
372
+ contextMatch: 0.8, // When retrieval context provided
373
+ moodCongruence: 0.7, // When mood provided
335
374
  provenance: { // When includeProvenance: true
336
375
  evidenceEpisodeIds: ['01XYZ...', '01DEF...'],
337
376
  evidenceCount: 3,
@@ -467,6 +506,8 @@ brain.on('decay', ({ totalEvaluated, transitionedToDormant }) => { ... });
467
506
  brain.on('rollback', ({ runId, rolledBackMemories }) => { ... });
468
507
  brain.on('forget', ({ id, type, purged }) => { ... });
469
508
  brain.on('purge', ({ episodes, semantics, procedures }) => { ... });
509
+ brain.on('interference', ({ newEpisodeId, suppressedId, similarity }) => { ... });
510
+ brain.on('resonance', ({ episodeId, resonances }) => { ... });
470
511
  brain.on('migration', ({ episodes, semantics, procedures }) => { ... });
471
512
  brain.on('error', (err) => { ... });
472
513
  ```
@@ -492,6 +533,9 @@ src/
492
533
  decay.js Ebbinghaus forgetting curves.
493
534
  embedding.js Pluggable providers (Mock, OpenAI). Batch embedding.
494
535
  encode.js Immutable episodic memory creation + vec0 writes.
536
+ affect.js Emotional memory: arousal-salience coupling, mood-congruent recall, resonance.
537
+ context.js Context-dependent retrieval modifier (encoding specificity).
538
+ interference.js Competitive memory suppression (engram competition).
495
539
  forget.js Soft-delete, hard-delete, query-based forget, bulk purge.
496
540
  introspect.js Health dashboard queries.
497
541
  llm.js Pluggable LLM providers (Mock, Anthropic, OpenAI).
@@ -531,7 +575,7 @@ All mutations use SQLite transactions. CHECK constraints enforce valid states an
531
575
  ## Running Tests
532
576
 
533
577
  ```bash
534
- npm test # 278 tests across 23 files
578
+ npm test # 379 tests across 28 files
535
579
  npm run test:watch
536
580
  ```
537
581
 
@@ -547,7 +591,29 @@ Demonstrates the full pipeline: encode 3 rate-limit observations, consolidate in
547
591
 
548
592
  ## Changelog
549
593
 
550
- ### v0.6.0 — Filtered Recall + Forget (current)
594
+ ### v0.9.0 — Emotional Memory (current)
595
+
596
+ - Valence-arousal affect model (Russell's circumplex) on every episode
597
+ - Arousal-salience coupling via Yerkes-Dodson inverted-U curve
598
+ - Mood-congruent recall — matching emotional state boosts retrieval confidence
599
+ - Emotional resonance detection — new experiences that echo past emotional patterns emit events
600
+ - MCP server: `memory_encode` accepts `affect`, `memory_recall` accepts `mood`
601
+ - 379 tests across 28 test files
602
+
603
+ ### v0.8.0 — Context-Dependent Retrieval
604
+
605
+ - Encoding specificity principle: context stored with memory, matching context boosts recall
606
+ - MCP server: `memory_encode` and `memory_recall` accept `context`
607
+ - 340 tests across 27 test files
608
+
609
+ ### v0.7.0 — Interference + Salience
610
+
611
+ - Interference-based forgetting: new memories competitively suppress similar existing ones
612
+ - Salience-weighted confidence: high-salience memories resist decay
613
+ - Spaced-repetition reconsolidation: retrieval intervals affect reinforcement strength
614
+ - 310 tests across 25 test files
615
+
616
+ ### v0.6.0 — Filtered Recall + Forget
551
617
 
552
618
  - Filtered recall: tag, source, and date-range filters on `recall()` and `recallStream()`
553
619
  - `forget()` — soft-delete any memory by ID
@@ -608,6 +674,8 @@ Demonstrates the full pipeline: encode 3 rate-limit observations, consolidate in
608
674
 
609
675
  **Why soft-delete by default?** Hard-deletes are irreversible. Soft-delete preserves data integrity and audit trails while excluding the memory from recall. Use `purge: true` or `brain.purge()` when you need permanent removal (GDPR, storage cleanup).
610
676
 
677
+ **Why emotional memory?** Every memory system stores facts. Biological memory stores facts with emotional context — and that context changes how memories are retrieved. Emotional arousal modulates encoding strength (amygdala-hippocampal interaction). Current mood biases which memories surface (Bower, 1981). This isn't a novelty feature — it's the foundation for AI that remembers like it cares.
678
+
611
679
  ## License
612
680
 
613
681
  MIT
@@ -1,7 +1,7 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { join } from 'node:path';
3
3
 
4
- export const VERSION = '0.8.0';
4
+ export const VERSION = '0.9.0';
5
5
  export const SERVER_NAME = 'audrey-memory';
6
6
  export const DEFAULT_DATA_DIR = join(homedir(), '.audrey', 'data');
7
7
 
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
@@ -164,10 +164,15 @@ async function main() {
164
164
  tags: z.array(z.string()).optional().describe('Optional tags for categorization'),
165
165
  salience: z.number().min(0).max(1).optional().describe('Importance weight 0-1'),
166
166
  context: z.record(z.string()).optional().describe('Situational context as key-value pairs (e.g., {task: "debugging", domain: "payments"})'),
167
+ affect: z.object({
168
+ valence: z.number().min(-1).max(1).describe('Emotional valence: -1 (very negative) to 1 (very positive)'),
169
+ arousal: z.number().min(0).max(1).optional().describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
170
+ label: z.string().optional().describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
171
+ }).optional().describe('Emotional affect — how this memory feels'),
167
172
  },
168
- async ({ content, source, tags, salience, context }) => {
173
+ async ({ content, source, tags, salience, context, affect }) => {
169
174
  try {
170
- const id = await audrey.encode({ content, source, tags, salience, context });
175
+ const id = await audrey.encode({ content, source, tags, salience, context, affect });
171
176
  return toolResult({ id, content, source });
172
177
  } catch (err) {
173
178
  return toolError(err);
@@ -187,8 +192,12 @@ async function main() {
187
192
  after: z.string().optional().describe('Only return memories created after this ISO date'),
188
193
  before: z.string().optional().describe('Only return memories created before this ISO date'),
189
194
  context: z.record(z.string()).optional().describe('Retrieval context — memories encoded in matching context get boosted'),
195
+ mood: z.object({
196
+ valence: z.number().min(-1).max(1).describe('Current emotional valence: -1 (negative) to 1 (positive)'),
197
+ arousal: z.number().min(0).max(1).optional().describe('Current arousal: 0 (calm) to 1 (activated)'),
198
+ }).optional().describe('Current mood — boosts recall of memories encoded in similar emotional state'),
190
199
  },
191
- async ({ query, limit, types, min_confidence, tags, sources, after, before, context }) => {
200
+ async ({ query, limit, types, min_confidence, tags, sources, after, before, context, mood }) => {
192
201
  try {
193
202
  const results = await audrey.recall(query, {
194
203
  limit: limit ?? 10,
@@ -199,6 +208,7 @@ async function main() {
199
208
  after,
200
209
  before,
201
210
  context,
211
+ mood,
202
212
  });
203
213
  return toolResult(results);
204
214
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audrey",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Biological memory architecture for AI agents — encode, consolidate, and recall memories with confidence decay, contradiction detection, and causal graphs",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/affect.js ADDED
@@ -0,0 +1,64 @@
1
+ export function arousalSalienceBoost(arousal) {
2
+ if (arousal === undefined || arousal === null) return 0;
3
+ // Inverted-U (Yerkes-Dodson): peaks at 0.7, Gaussian sigma=0.3
4
+ return Math.exp(-Math.pow(arousal - 0.7, 2) / (2 * 0.3 * 0.3));
5
+ }
6
+
7
+ export function affectSimilarity(a, b) {
8
+ if (!a || !b) return 0;
9
+ if (a.valence === undefined || b.valence === undefined) return 0;
10
+ const valenceDist = Math.abs(a.valence - b.valence);
11
+ const valenceSim = 1.0 - (valenceDist / 2.0);
12
+ if (a.arousal === undefined || b.arousal === undefined) return valenceSim;
13
+ const arousalSim = 1.0 - Math.abs(a.arousal - b.arousal);
14
+ // Valence is primary (70%), arousal secondary (30%) per Bower 1981
15
+ return 0.7 * valenceSim + 0.3 * arousalSim;
16
+ }
17
+
18
+ export function moodCongruenceModifier(encodingAffect, retrievalMood, weight = 0.2) {
19
+ if (!encodingAffect || !retrievalMood) return 1.0;
20
+ const similarity = affectSimilarity(encodingAffect, retrievalMood);
21
+ if (similarity === 0) return 1.0;
22
+ return 1.0 + (weight * similarity);
23
+ }
24
+
25
+ export async function detectResonance(db, embeddingProvider, episodeId, { content, affect }, config = {}) {
26
+ const { enabled = true, k = 5, threshold = 0.5, affectThreshold = 0.6 } = config;
27
+ if (!enabled || !affect || affect.valence === undefined) return [];
28
+
29
+ const vector = await embeddingProvider.embed(content);
30
+ const buffer = embeddingProvider.vectorToBuffer(vector);
31
+
32
+ const matches = db.prepare(`
33
+ SELECT e.*, (1.0 - v.distance) AS similarity
34
+ FROM vec_episodes v
35
+ JOIN episodes e ON e.id = v.id
36
+ WHERE v.embedding MATCH ?
37
+ AND k = ?
38
+ AND e.id != ?
39
+ AND e.superseded_by IS NULL
40
+ `).all(buffer, k, episodeId);
41
+
42
+ const resonances = [];
43
+ for (const match of matches) {
44
+ if (match.similarity < threshold) continue;
45
+ let priorAffect;
46
+ try { priorAffect = JSON.parse(match.affect || '{}'); } catch { continue; }
47
+ if (priorAffect.valence === undefined) continue;
48
+
49
+ const emotionalSimilarity = affectSimilarity(affect, priorAffect);
50
+ if (emotionalSimilarity < affectThreshold) continue;
51
+
52
+ resonances.push({
53
+ priorEpisodeId: match.id,
54
+ priorContent: match.content,
55
+ priorAffect,
56
+ semanticSimilarity: match.similarity,
57
+ emotionalSimilarity,
58
+ timeDelta: Date.now() - new Date(match.created_at).getTime(),
59
+ priorCreatedAt: match.created_at,
60
+ });
61
+ }
62
+
63
+ return resonances;
64
+ }
package/src/audrey.js CHANGED
@@ -16,6 +16,7 @@ import { importMemories } from './import.js';
16
16
  import { suggestConsolidationParams as suggestParamsFn } from './adaptive.js';
17
17
  import { reembedAll } from './migrate.js';
18
18
  import { applyInterference } from './interference.js';
19
+ import { detectResonance } from './affect.js';
19
20
 
20
21
  /**
21
22
  * @typedef {'direct-observation' | 'told-by-user' | 'tool-result' | 'inference' | 'model-generated'} SourceType
@@ -92,6 +93,7 @@ export class Audrey extends EventEmitter {
92
93
  decay = {},
93
94
  interference = {},
94
95
  context = {},
96
+ affect = {},
95
97
  } = {}) {
96
98
  super();
97
99
 
@@ -118,6 +120,7 @@ export class Audrey extends EventEmitter {
118
120
  sourceReliability: confidence.sourceReliability,
119
121
  interferenceWeight: interference.weight ?? 0.1,
120
122
  contextWeight: context.weight ?? 0.3,
123
+ affectWeight: affect.weight ?? 0.2,
121
124
  };
122
125
  this.consolidationConfig = {
123
126
  minEpisodes: consolidation.minEpisodes || 3,
@@ -134,6 +137,17 @@ export class Audrey extends EventEmitter {
134
137
  enabled: context.enabled ?? true,
135
138
  weight: context.weight ?? 0.3,
136
139
  };
140
+ this.affectConfig = {
141
+ enabled: affect.enabled ?? true,
142
+ weight: affect.weight ?? 0.2,
143
+ arousalWeight: affect.arousalWeight ?? 0.3,
144
+ resonance: {
145
+ enabled: affect.resonance?.enabled ?? true,
146
+ k: affect.resonance?.k ?? 5,
147
+ threshold: affect.resonance?.threshold ?? 0.5,
148
+ affectThreshold: affect.resonance?.affectThreshold ?? 0.6,
149
+ },
150
+ };
137
151
  }
138
152
 
139
153
  async _ensureMigrated() {
@@ -173,7 +187,8 @@ export class Audrey extends EventEmitter {
173
187
  */
174
188
  async encode(params) {
175
189
  await this._ensureMigrated();
176
- const id = await encodeEpisode(this.db, this.embeddingProvider, params);
190
+ const encodeParams = { ...params, arousalWeight: this.affectConfig.arousalWeight };
191
+ const id = await encodeEpisode(this.db, this.embeddingProvider, encodeParams);
177
192
  this.emit('encode', { id, ...params });
178
193
  if (this.interferenceConfig.enabled) {
179
194
  applyInterference(this.db, this.embeddingProvider, id, params, this.interferenceConfig)
@@ -184,6 +199,15 @@ export class Audrey extends EventEmitter {
184
199
  })
185
200
  .catch(err => this.emit('error', err));
186
201
  }
202
+ if (this.affectConfig.enabled && this.affectConfig.resonance.enabled && params.affect?.valence !== undefined) {
203
+ detectResonance(this.db, this.embeddingProvider, id, params, this.affectConfig.resonance)
204
+ .then(echoes => {
205
+ if (echoes.length > 0) {
206
+ this.emit('resonance', { episodeId: id, affect: params.affect, echoes });
207
+ }
208
+ })
209
+ .catch(err => this.emit('error', err));
210
+ }
187
211
  this._emitValidation(id, params);
188
212
  return id;
189
213
  }
@@ -235,10 +259,14 @@ export class Audrey extends EventEmitter {
235
259
  }
236
260
 
237
261
  _recallConfig(options) {
238
- const base = options.confidenceConfig ?? this.confidenceConfig;
239
- return this.contextConfig.enabled && options.context
240
- ? { ...base, retrievalContext: options.context }
241
- : base;
262
+ let config = options.confidenceConfig ?? this.confidenceConfig;
263
+ if (this.contextConfig.enabled && options.context) {
264
+ config = { ...config, retrievalContext: options.context };
265
+ }
266
+ if (this.affectConfig.enabled && options.mood) {
267
+ config = { ...config, retrievalMood: options.mood };
268
+ }
269
+ return config;
242
270
  }
243
271
 
244
272
  /**
package/src/db.js CHANGED
@@ -12,6 +12,7 @@ const SCHEMA = `
12
12
  source_reliability REAL NOT NULL,
13
13
  salience REAL DEFAULT 0.5,
14
14
  context TEXT DEFAULT '{}',
15
+ affect TEXT DEFAULT '{}',
15
16
  tags TEXT,
16
17
  causal_trigger TEXT,
17
18
  causal_consequence TEXT,
package/src/encode.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { generateId } from './ulid.js';
2
2
  import { sourceReliability } from './confidence.js';
3
+ import { arousalSalienceBoost } from './affect.js';
3
4
 
4
5
  /**
5
6
  * @param {import('better-sqlite3').Database} db
@@ -15,6 +16,8 @@ export async function encodeEpisode(db, embeddingProvider, {
15
16
  tags,
16
17
  supersedes,
17
18
  context = {},
19
+ affect = {},
20
+ arousalWeight = 0.3,
18
21
  }) {
19
22
  if (!content || typeof content !== 'string') throw new Error('content must be a non-empty string');
20
23
  if (salience < 0 || salience > 1) throw new Error('salience must be between 0 and 1');
@@ -26,16 +29,20 @@ export async function encodeEpisode(db, embeddingProvider, {
26
29
  const id = generateId();
27
30
  const now = new Date().toISOString();
28
31
 
32
+ const boost = arousalSalienceBoost(affect.arousal);
33
+ const effectiveSalience = Math.min(1.0, salience + (boost * arousalWeight));
34
+
29
35
  const insertAndLink = db.transaction(() => {
30
36
  db.prepare(`
31
37
  INSERT INTO episodes (
32
- id, content, embedding, source, source_reliability, salience, context,
38
+ id, content, embedding, source, source_reliability, salience, context, affect,
33
39
  tags, causal_trigger, causal_consequence, created_at,
34
40
  embedding_model, embedding_version, supersedes
35
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
41
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
36
42
  `).run(
37
- id, content, embeddingBuffer, source, reliability, salience,
43
+ id, content, embeddingBuffer, source, reliability, effectiveSalience,
38
44
  JSON.stringify(context),
45
+ JSON.stringify(affect),
39
46
  tags ? JSON.stringify(tags) : null,
40
47
  causal?.trigger || null, causal?.consequence || null,
41
48
  now, embeddingProvider.modelName, embeddingProvider.modelVersion,
package/src/index.js CHANGED
@@ -17,3 +17,4 @@ export { reembedAll } from './migrate.js';
17
17
  export { forgetMemory, forgetByQuery, purgeMemories } from './forget.js';
18
18
  export { applyInterference, interferenceModifier } from './interference.js';
19
19
  export { contextMatchRatio, contextModifier } from './context.js';
20
+ export { arousalSalienceBoost, affectSimilarity, moodCongruenceModifier, detectResonance } from './affect.js';
package/src/recall.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { computeConfidence, DEFAULT_HALF_LIVES, salienceModifier } from './confidence.js';
2
2
  import { interferenceModifier } from './interference.js';
3
3
  import { contextMatchRatio, contextModifier } from './context.js';
4
+ import { moodCongruenceModifier, affectSimilarity } from './affect.js';
4
5
  import { daysBetween, safeJsonParse } from './utils.js';
5
6
 
6
7
  function computeEpisodicConfidence(ep, now, confidenceConfig = {}) {
@@ -65,7 +66,7 @@ function computeProceduralConfidence(proc, now, confidenceConfig = {}) {
65
66
  return Math.max(0, Math.min(1, confidence));
66
67
  }
67
68
 
68
- function buildEpisodicEntry(ep, confidence, score, includeProvenance, contextMatch) {
69
+ function buildEpisodicEntry(ep, confidence, score, includeProvenance, contextMatch, moodCongruence) {
69
70
  const entry = {
70
71
  id: ep.id,
71
72
  content: ep.content,
@@ -78,6 +79,9 @@ function buildEpisodicEntry(ep, confidence, score, includeProvenance, contextMat
78
79
  if (contextMatch !== undefined) {
79
80
  entry.contextMatch = contextMatch;
80
81
  }
82
+ if (moodCongruence !== undefined) {
83
+ entry.moodCongruence = moodCongruence;
84
+ }
81
85
  if (includeProvenance) {
82
86
  entry.provenance = {
83
87
  source: ep.source,
@@ -174,9 +178,17 @@ function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includePro
174
178
  confidence = Math.max(0, Math.min(1, confidence));
175
179
  }
176
180
 
181
+ let moodMatch;
182
+ if (confidenceConfig?.retrievalMood) {
183
+ const encodingAffect = safeJsonParse(row.affect, {});
184
+ moodMatch = affectSimilarity(encodingAffect, confidenceConfig.retrievalMood);
185
+ confidence *= moodCongruenceModifier(encodingAffect, confidenceConfig.retrievalMood, confidenceConfig.affectWeight);
186
+ confidence = Math.max(0, Math.min(1, confidence));
187
+ }
188
+
177
189
  if (confidence < minConfidence) continue;
178
190
  const score = row.similarity * confidence;
179
- results.push(buildEpisodicEntry(row, confidence, score, includeProvenance, ctxMatch));
191
+ results.push(buildEpisodicEntry(row, confidence, score, includeProvenance, ctxMatch, moodMatch));
180
192
  }
181
193
  return results;
182
194
  }