audrey 0.21.0 → 0.23.1

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 (152) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/README.md +76 -32
  3. package/SECURITY.md +29 -0
  4. package/dist/mcp-server/config.d.ts +5 -4
  5. package/dist/mcp-server/config.d.ts.map +1 -1
  6. package/dist/mcp-server/config.js +6 -8
  7. package/dist/mcp-server/config.js.map +1 -1
  8. package/dist/mcp-server/index.d.ts +219 -23
  9. package/dist/mcp-server/index.d.ts.map +1 -1
  10. package/dist/mcp-server/index.js +681 -95
  11. package/dist/mcp-server/index.js.map +1 -1
  12. package/dist/src/adaptive.d.ts.map +1 -1
  13. package/dist/src/adaptive.js +3 -1
  14. package/dist/src/adaptive.js.map +1 -1
  15. package/dist/src/affect.d.ts +4 -1
  16. package/dist/src/affect.d.ts.map +1 -1
  17. package/dist/src/affect.js +6 -4
  18. package/dist/src/affect.js.map +1 -1
  19. package/dist/src/audrey.d.ts +54 -4
  20. package/dist/src/audrey.d.ts.map +1 -1
  21. package/dist/src/audrey.js +457 -62
  22. package/dist/src/audrey.js.map +1 -1
  23. package/dist/src/capsule.d.ts +2 -1
  24. package/dist/src/capsule.d.ts.map +1 -1
  25. package/dist/src/capsule.js +14 -4
  26. package/dist/src/capsule.js.map +1 -1
  27. package/dist/src/causal.d.ts.map +1 -1
  28. package/dist/src/causal.js +20 -2
  29. package/dist/src/causal.js.map +1 -1
  30. package/dist/src/confidence.d.ts.map +1 -1
  31. package/dist/src/confidence.js +3 -0
  32. package/dist/src/confidence.js.map +1 -1
  33. package/dist/src/consolidate.d.ts +1 -0
  34. package/dist/src/consolidate.d.ts.map +1 -1
  35. package/dist/src/consolidate.js +35 -19
  36. package/dist/src/consolidate.js.map +1 -1
  37. package/dist/src/controller.d.ts +38 -0
  38. package/dist/src/controller.d.ts.map +1 -0
  39. package/dist/src/controller.js +169 -0
  40. package/dist/src/controller.js.map +1 -0
  41. package/dist/src/db.d.ts.map +1 -1
  42. package/dist/src/db.js +12 -0
  43. package/dist/src/db.js.map +1 -1
  44. package/dist/src/decay.d.ts.map +1 -1
  45. package/dist/src/decay.js +57 -50
  46. package/dist/src/decay.js.map +1 -1
  47. package/dist/src/embedding.d.ts.map +1 -1
  48. package/dist/src/embedding.js +31 -3
  49. package/dist/src/embedding.js.map +1 -1
  50. package/dist/src/encode.d.ts +9 -2
  51. package/dist/src/encode.d.ts.map +1 -1
  52. package/dist/src/encode.js +21 -8
  53. package/dist/src/encode.js.map +1 -1
  54. package/dist/src/export.d.ts.map +1 -1
  55. package/dist/src/export.js +5 -3
  56. package/dist/src/export.js.map +1 -1
  57. package/dist/src/feedback.d.ts +29 -0
  58. package/dist/src/feedback.d.ts.map +1 -0
  59. package/dist/src/feedback.js +123 -0
  60. package/dist/src/feedback.js.map +1 -0
  61. package/dist/src/forget.d.ts.map +1 -1
  62. package/dist/src/forget.js +58 -50
  63. package/dist/src/forget.js.map +1 -1
  64. package/dist/src/fts.js +1 -1
  65. package/dist/src/fts.js.map +1 -1
  66. package/dist/src/hybrid-recall.d.ts +2 -1
  67. package/dist/src/hybrid-recall.d.ts.map +1 -1
  68. package/dist/src/hybrid-recall.js +35 -26
  69. package/dist/src/hybrid-recall.js.map +1 -1
  70. package/dist/src/impact.d.ts +47 -0
  71. package/dist/src/impact.d.ts.map +1 -0
  72. package/dist/src/impact.js +146 -0
  73. package/dist/src/impact.js.map +1 -0
  74. package/dist/src/import.d.ts +177 -1
  75. package/dist/src/import.d.ts.map +1 -1
  76. package/dist/src/import.js +206 -17
  77. package/dist/src/import.js.map +1 -1
  78. package/dist/src/index.d.ts +4 -0
  79. package/dist/src/index.d.ts.map +1 -1
  80. package/dist/src/index.js +2 -0
  81. package/dist/src/index.js.map +1 -1
  82. package/dist/src/interference.d.ts +5 -2
  83. package/dist/src/interference.d.ts.map +1 -1
  84. package/dist/src/interference.js +27 -20
  85. package/dist/src/interference.js.map +1 -1
  86. package/dist/src/llm.d.ts.map +1 -1
  87. package/dist/src/llm.js +1 -0
  88. package/dist/src/llm.js.map +1 -1
  89. package/dist/src/migrate.d.ts.map +1 -1
  90. package/dist/src/migrate.js +21 -9
  91. package/dist/src/migrate.js.map +1 -1
  92. package/dist/src/preflight.d.ts +2 -1
  93. package/dist/src/preflight.d.ts.map +1 -1
  94. package/dist/src/preflight.js +25 -5
  95. package/dist/src/preflight.js.map +1 -1
  96. package/dist/src/profile.d.ts +23 -0
  97. package/dist/src/profile.d.ts.map +1 -0
  98. package/dist/src/profile.js +51 -0
  99. package/dist/src/profile.js.map +1 -0
  100. package/dist/src/promote.d.ts.map +1 -1
  101. package/dist/src/promote.js +2 -3
  102. package/dist/src/promote.js.map +1 -1
  103. package/dist/src/prompts.d.ts.map +1 -1
  104. package/dist/src/prompts.js +76 -47
  105. package/dist/src/prompts.js.map +1 -1
  106. package/dist/src/recall.d.ts +9 -6
  107. package/dist/src/recall.d.ts.map +1 -1
  108. package/dist/src/recall.js +182 -40
  109. package/dist/src/recall.js.map +1 -1
  110. package/dist/src/redact.d.ts +7 -1
  111. package/dist/src/redact.d.ts.map +1 -1
  112. package/dist/src/redact.js +94 -11
  113. package/dist/src/redact.js.map +1 -1
  114. package/dist/src/rollback.d.ts.map +1 -1
  115. package/dist/src/rollback.js +9 -4
  116. package/dist/src/rollback.js.map +1 -1
  117. package/dist/src/routes.d.ts +1 -0
  118. package/dist/src/routes.d.ts.map +1 -1
  119. package/dist/src/routes.js +185 -6
  120. package/dist/src/routes.js.map +1 -1
  121. package/dist/src/rules-compiler.d.ts.map +1 -1
  122. package/dist/src/rules-compiler.js +36 -6
  123. package/dist/src/rules-compiler.js.map +1 -1
  124. package/dist/src/server.d.ts +2 -1
  125. package/dist/src/server.d.ts.map +1 -1
  126. package/dist/src/server.js +42 -4
  127. package/dist/src/server.js.map +1 -1
  128. package/dist/src/tool-trace.d.ts.map +1 -1
  129. package/dist/src/tool-trace.js +42 -29
  130. package/dist/src/tool-trace.js.map +1 -1
  131. package/dist/src/types.d.ts +28 -1
  132. package/dist/src/types.d.ts.map +1 -1
  133. package/dist/src/ulid.d.ts.map +1 -1
  134. package/dist/src/ulid.js +52 -2
  135. package/dist/src/ulid.js.map +1 -1
  136. package/dist/src/utils.d.ts.map +1 -1
  137. package/dist/src/utils.js +8 -1
  138. package/dist/src/utils.js.map +1 -1
  139. package/dist/src/validate.d.ts +2 -0
  140. package/dist/src/validate.d.ts.map +1 -1
  141. package/dist/src/validate.js +60 -29
  142. package/dist/src/validate.js.map +1 -1
  143. package/package.json +20 -26
  144. package/docs/assets/benchmarks/local-benchmark.svg +0 -45
  145. package/docs/assets/benchmarks/operations-benchmark.svg +0 -45
  146. package/docs/assets/benchmarks/published-memory-standards.svg +0 -50
  147. package/docs/audrey-for-dummies.md +0 -670
  148. package/docs/benchmarking.md +0 -151
  149. package/docs/future-of-llm-memory.md +0 -452
  150. package/docs/mcp-hosts.md +0 -206
  151. package/docs/ollama-local-agents.md +0 -128
  152. package/docs/production-readiness.md +0 -128
@@ -9,6 +9,8 @@ import { runConsolidation } from './consolidate.js';
9
9
  import { applyDecay } from './decay.js';
10
10
  import { rollbackConsolidation, getConsolidationHistory } from './rollback.js';
11
11
  import { forgetMemory, forgetByQuery as forgetByQueryFn, purgeMemories } from './forget.js';
12
+ import { applyFeedback } from './feedback.js';
13
+ import { buildImpactReport } from './impact.js';
12
14
  import { introspect as introspectFn } from './introspect.js';
13
15
  import { buildContextResolutionPrompt, buildReflectionPrompt } from './prompts.js';
14
16
  import { exportMemories } from './export.js';
@@ -26,7 +28,95 @@ import { findPromotionCandidates, } from './promote.js';
26
28
  import { renderAllRules } from './rules-compiler.js';
27
29
  import { insertEvent } from './events.js';
28
30
  import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
29
- import { dirname, join, resolve as pathResolve } from 'node:path';
31
+ import { dirname, join, resolve as pathResolve, relative, isAbsolute as pathIsAbsolute } from 'node:path';
32
+ import { ProfileRecorder } from './profile.js';
33
+ import { performance } from 'node:perf_hooks';
34
+ function roundMs(value) {
35
+ return Math.round(value * 1000) / 1000;
36
+ }
37
+ function validateEncodeParams(params) {
38
+ if (!params.content || typeof params.content !== 'string') {
39
+ throw new Error('content must be a non-empty string');
40
+ }
41
+ if (params.salience !== undefined && (params.salience < 0 || params.salience > 1)) {
42
+ throw new Error('salience must be between 0 and 1');
43
+ }
44
+ if (params.tags !== undefined && !Array.isArray(params.tags)) {
45
+ throw new Error('tags must be an array');
46
+ }
47
+ }
48
+ const REFLECTION_SOURCES = new Set([
49
+ 'direct-observation',
50
+ 'told-by-user',
51
+ 'inference',
52
+ ]);
53
+ function boundedString(value, maxLength) {
54
+ if (typeof value !== 'string')
55
+ return undefined;
56
+ const trimmed = value.trim();
57
+ if (!trimmed)
58
+ return undefined;
59
+ return trimmed.slice(0, maxLength);
60
+ }
61
+ function boundedNumber(value, min, max) {
62
+ if (typeof value !== 'number' || !Number.isFinite(value))
63
+ return undefined;
64
+ return Math.max(min, Math.min(max, value));
65
+ }
66
+ function normalizeReflectionAffect(raw) {
67
+ if (!raw || typeof raw !== 'object')
68
+ return undefined;
69
+ const record = raw;
70
+ const valence = boundedNumber(record.valence, -1, 1);
71
+ const arousal = boundedNumber(record.arousal, 0, 1);
72
+ if (valence === undefined && arousal === undefined)
73
+ return undefined;
74
+ const affect = {};
75
+ if (valence !== undefined)
76
+ affect.valence = valence;
77
+ if (arousal !== undefined)
78
+ affect.arousal = arousal;
79
+ const label = boundedString(record.label, 64);
80
+ if (label)
81
+ affect.label = label;
82
+ return affect;
83
+ }
84
+ function normalizeReflectionMemory(raw) {
85
+ if (!raw || typeof raw !== 'object')
86
+ return null;
87
+ const record = raw;
88
+ const content = boundedString(record.content, 5000);
89
+ if (!content)
90
+ return null;
91
+ const source = record.source;
92
+ if (typeof source !== 'string' || !REFLECTION_SOURCES.has(source)) {
93
+ return null;
94
+ }
95
+ const memory = {
96
+ content,
97
+ source: source,
98
+ };
99
+ const salience = boundedNumber(record.salience, 0, 1);
100
+ if (salience !== undefined)
101
+ memory.salience = salience;
102
+ if (Array.isArray(record.tags)) {
103
+ const tags = record.tags
104
+ .map(tag => boundedString(tag, 64))
105
+ .filter((tag) => Boolean(tag))
106
+ .slice(0, 20);
107
+ if (tags.length > 0)
108
+ memory.tags = tags;
109
+ }
110
+ if (typeof record.private === 'boolean')
111
+ memory.private = record.private;
112
+ const affect = normalizeReflectionAffect(record.affect);
113
+ if (affect)
114
+ memory.affect = affect;
115
+ return memory;
116
+ }
117
+ function messagesToLegacyPrompt(messages) {
118
+ return messages.map(message => `${message.role.toUpperCase()}:\n${message.content}`).join('\n\n');
119
+ }
30
120
  export class Audrey extends EventEmitter {
31
121
  agent;
32
122
  dataDir;
@@ -39,10 +129,18 @@ export class Audrey extends EventEmitter {
39
129
  interferenceConfig;
40
130
  contextConfig;
41
131
  affectConfig;
132
+ defaultRetrievalMode;
42
133
  autoReflect;
43
134
  _migrationPending;
44
135
  _autoConsolidateTimer;
45
136
  _closed;
137
+ _postEncodeQueue;
138
+ _pendingPostEncodeIds;
139
+ _embeddingWarm;
140
+ _embeddingWarmupPromise;
141
+ _warmupDurationMs;
142
+ _lastRecallCheckAt;
143
+ _lastRecallErrors;
46
144
  constructor({ dataDir = './audrey-data', agent = 'default', embedding = { provider: 'mock', dimensions: 64 }, llm, confidence = {}, consolidation = {}, decay = {}, interference = {}, context = {}, affect = {}, autoReflect = false, } = {}) {
47
145
  super();
48
146
  const dormantThreshold = decay.dormantThreshold ?? 0.1;
@@ -69,9 +167,9 @@ export class Audrey extends EventEmitter {
69
167
  affectWeight: affect.weight ?? 0.2,
70
168
  };
71
169
  this.consolidationConfig = {
72
- minEpisodes: consolidation.minEpisodes || 3,
170
+ minEpisodes,
73
171
  };
74
- this.decayConfig = { dormantThreshold: decay.dormantThreshold || 0.1 };
172
+ this.decayConfig = { dormantThreshold };
75
173
  this._autoConsolidateTimer = null;
76
174
  this._closed = false;
77
175
  this.interferenceConfig = {
@@ -95,7 +193,15 @@ export class Audrey extends EventEmitter {
95
193
  affectThreshold: affect.resonance?.affectThreshold ?? 0.6,
96
194
  },
97
195
  };
196
+ this.defaultRetrievalMode = 'hybrid';
98
197
  this.autoReflect = autoReflect;
198
+ this._postEncodeQueue = Promise.resolve();
199
+ this._pendingPostEncodeIds = new Set();
200
+ this._embeddingWarm = false;
201
+ this._embeddingWarmupPromise = null;
202
+ this._warmupDurationMs = null;
203
+ this._lastRecallCheckAt = null;
204
+ this._lastRecallErrors = [];
99
205
  }
100
206
  async _ensureMigrated() {
101
207
  if (!this._migrationPending)
@@ -104,54 +210,207 @@ export class Audrey extends EventEmitter {
104
210
  this._migrationPending = false;
105
211
  this.emit('migration', counts);
106
212
  }
107
- _emitValidation(id, params) {
108
- validateMemory(this.db, this.embeddingProvider, { id, ...params }, {
109
- llmProvider: this.llmProvider,
213
+ startEmbeddingWarmup(text = 'warmup') {
214
+ if (this._embeddingWarm)
215
+ return Promise.resolve();
216
+ if (this._embeddingWarmupPromise)
217
+ return this._embeddingWarmupPromise;
218
+ const startedAt = performance.now();
219
+ this._embeddingWarmupPromise = (async () => {
220
+ if (typeof this.embeddingProvider.ready === 'function') {
221
+ await this.embeddingProvider.ready();
222
+ }
223
+ await this.embeddingProvider.embed(text);
224
+ this._embeddingWarm = true;
225
+ })()
226
+ .catch(err => {
227
+ this._emitQueueError(err);
228
+ throw err;
110
229
  })
111
- .then(validation => {
112
- if (validation.action === 'reinforced') {
113
- this.emit('reinforcement', {
114
- episodeId: id,
115
- targetId: validation.semanticId,
116
- similarity: validation.similarity,
117
- });
230
+ .finally(() => {
231
+ this._warmupDurationMs = roundMs(performance.now() - startedAt);
232
+ });
233
+ return this._embeddingWarmupPromise;
234
+ }
235
+ async _waitForEmbeddingWarmup(profile, spanName = 'embedding.wait_for_warmup') {
236
+ if (!this._embeddingWarmupPromise || this._embeddingWarm)
237
+ return;
238
+ const wait = async () => {
239
+ try {
240
+ await this._embeddingWarmupPromise;
118
241
  }
119
- else if (validation.action === 'contradiction') {
120
- this.emit('contradiction', {
121
- episodeId: id,
122
- contradictionId: validation.contradictionId,
123
- semanticId: validation.semanticId,
124
- similarity: validation.similarity,
125
- resolution: validation.resolution,
126
- });
242
+ catch {
243
+ // Warmup failure should not poison the foreground call; the foreground
244
+ // embed path will surface provider errors if the provider is truly broken.
127
245
  }
128
- })
129
- .catch(err => this.emit('error', err));
246
+ };
247
+ if (profile)
248
+ await profile.measure(spanName, wait);
249
+ else
250
+ await wait();
130
251
  }
131
- async encode(params) {
132
- await this._ensureMigrated();
133
- const encodeParams = { ...params, arousalWeight: this.affectConfig.arousalWeight };
134
- const id = await encodeEpisode(this.db, this.embeddingProvider, encodeParams);
135
- this.emit('encode', { id, ...params });
252
+ async _validateEncodedMemory(id, params, embedding) {
253
+ const validation = await validateMemory(this.db, this.embeddingProvider, { id, ...params }, {
254
+ llmProvider: this.llmProvider,
255
+ embeddingVector: embedding?.vector,
256
+ embeddingBuffer: embedding?.buffer,
257
+ });
258
+ if (validation.action === 'reinforced') {
259
+ this.emit('reinforcement', {
260
+ episodeId: id,
261
+ targetId: validation.semanticId,
262
+ similarity: validation.similarity,
263
+ });
264
+ }
265
+ else if (validation.action === 'contradiction') {
266
+ this.emit('contradiction', {
267
+ episodeId: id,
268
+ contradictionId: validation.contradictionId,
269
+ semanticId: validation.semanticId,
270
+ similarity: validation.similarity,
271
+ resolution: validation.resolution,
272
+ });
273
+ }
274
+ }
275
+ async _runPostEncodeStage(name, run) {
276
+ try {
277
+ await run();
278
+ }
279
+ catch (err) {
280
+ this._emitQueueError(Object.assign(err instanceof Error ? err : new Error(String(err)), {
281
+ stage: name,
282
+ }));
283
+ }
284
+ }
285
+ async _runPostEncode(id, params, embedding) {
136
286
  if (this.interferenceConfig.enabled) {
137
- applyInterference(this.db, this.embeddingProvider, id, params, this.interferenceConfig)
138
- .then(affected => {
287
+ await this._runPostEncodeStage('interference', async () => {
288
+ const affected = await applyInterference(this.db, this.embeddingProvider, id, params, this.interferenceConfig, embedding);
139
289
  if (affected.length > 0) {
140
290
  this.emit('interference', { episodeId: id, affected });
141
291
  }
142
- })
143
- .catch(err => this.emit('error', err));
292
+ });
144
293
  }
145
294
  if (this.affectConfig.enabled && this.affectConfig.resonance.enabled && params.affect?.valence !== undefined) {
146
- detectResonance(this.db, this.embeddingProvider, id, params, this.affectConfig.resonance)
147
- .then(echoes => {
295
+ await this._runPostEncodeStage('resonance', async () => {
296
+ const echoes = await detectResonance(this.db, this.embeddingProvider, id, params, this.affectConfig.resonance, embedding);
148
297
  if (echoes.length > 0) {
149
298
  this.emit('resonance', { episodeId: id, affect: params.affect, echoes });
150
299
  }
151
- })
152
- .catch(err => this.emit('error', err));
300
+ });
301
+ }
302
+ await this._runPostEncodeStage('validation', async () => {
303
+ await this._validateEncodedMemory(id, params, embedding);
304
+ });
305
+ }
306
+ _enqueuePostEncode(id, params, embedding) {
307
+ const enqueuedAt = performance.now();
308
+ this._pendingPostEncodeIds.add(id);
309
+ const run = async () => {
310
+ const startedAt = performance.now();
311
+ try {
312
+ if (!this._closed) {
313
+ await this._runPostEncode(id, params, embedding);
314
+ }
315
+ }
316
+ finally {
317
+ const finishedAt = performance.now();
318
+ this._pendingPostEncodeIds.delete(id);
319
+ this.emit('post-encode-complete', {
320
+ episodeId: id,
321
+ queued_ms: roundMs(startedAt - enqueuedAt),
322
+ processing_ms: roundMs(finishedAt - startedAt),
323
+ total_ms: roundMs(finishedAt - enqueuedAt),
324
+ pending_consolidation_count: this._pendingPostEncodeIds.size,
325
+ });
326
+ }
327
+ };
328
+ const task = this._postEncodeQueue.then(run, run);
329
+ this._postEncodeQueue = task.catch(err => {
330
+ this._emitQueueError(err);
331
+ });
332
+ return task;
333
+ }
334
+ _emitQueueError(err) {
335
+ if (this.listenerCount('error') > 0) {
336
+ // Caller has opted into error handling; let them route logging.
337
+ this.emit('error', err);
338
+ return;
339
+ }
340
+ // Standard EventEmitter idiom: log only when nobody is listening, so we
341
+ // surface failures by default but don't double-log for apps with structured
342
+ // error pipelines. The MCP server registers a logger listener at startup.
343
+ const stage = err?.stage;
344
+ const prefix = stage ? `[audrey:post-encode:${stage}]` : '[audrey:post-encode]';
345
+ const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
346
+ console.error(`${prefix} ${message}`);
347
+ }
348
+ pendingConsolidationIds() {
349
+ return [...this._pendingPostEncodeIds];
350
+ }
351
+ async drainPostEncodeQueue(timeoutMs = 5000) {
352
+ if (this._pendingPostEncodeIds.size === 0) {
353
+ return { drained: true, pendingIds: [] };
354
+ }
355
+ let timeout;
356
+ const timedOut = Symbol('timed-out');
357
+ const timeoutPromise = new Promise(resolve => {
358
+ timeout = setTimeout(() => resolve(timedOut), timeoutMs);
359
+ });
360
+ const result = await Promise.race([
361
+ this._postEncodeQueue.then(() => true),
362
+ timeoutPromise,
363
+ ]);
364
+ if (timeout)
365
+ clearTimeout(timeout);
366
+ const drained = result === true && this._pendingPostEncodeIds.size === 0;
367
+ return {
368
+ drained,
369
+ pendingIds: this.pendingConsolidationIds(),
370
+ };
371
+ }
372
+ async encode(params) {
373
+ return this._encodeInternal(params);
374
+ }
375
+ async encodeWithDiagnostics(params) {
376
+ const profile = new ProfileRecorder('memory_encode');
377
+ const id = await this._encodeInternal(params, profile);
378
+ return { id, diagnostics: profile.finish() };
379
+ }
380
+ async _encodeInternal(params, profile) {
381
+ await this._waitForEmbeddingWarmup(profile, 'encode.wait_for_warmup');
382
+ if (profile)
383
+ await profile.measure('encode.ensure_migrated', () => this._ensureMigrated());
384
+ else
385
+ await this._ensureMigrated();
386
+ const encodeParams = { ...params, agent: params.agent ?? this.agent, arousalWeight: this.affectConfig.arousalWeight };
387
+ let encodedVector;
388
+ let encodedBuffer;
389
+ const id = profile
390
+ ? await profile.measure('encode.episode', () => encodeEpisode(this.db, this.embeddingProvider, encodeParams, {
391
+ profile,
392
+ onVector: (vector, buffer) => {
393
+ encodedVector = vector;
394
+ encodedBuffer = buffer;
395
+ },
396
+ }))
397
+ : await encodeEpisode(this.db, this.embeddingProvider, encodeParams, {
398
+ onVector: (vector, buffer) => {
399
+ encodedVector = vector;
400
+ encodedBuffer = buffer;
401
+ },
402
+ });
403
+ const encodedEmbedding = { vector: encodedVector, buffer: encodedBuffer };
404
+ this.emit('encode', { id, ...params });
405
+ const postEncodeTask = profile
406
+ ? profile.measureSync('encode.enqueue_background', () => this._enqueuePostEncode(id, params, encodedEmbedding))
407
+ : this._enqueuePostEncode(id, params, encodedEmbedding);
408
+ if (params.waitForConsolidation) {
409
+ if (profile)
410
+ await profile.measure('encode.wait_for_consolidation', () => postEncodeTask);
411
+ else
412
+ await postEncodeTask;
153
413
  }
154
- this._emitValidation(id, params);
155
414
  return id;
156
415
  }
157
416
  async reflect(turns) {
@@ -160,7 +419,15 @@ export class Audrey extends EventEmitter {
160
419
  const prompt = buildReflectionPrompt(turns);
161
420
  let raw;
162
421
  try {
163
- raw = await this.llmProvider.chat(prompt);
422
+ if (typeof this.llmProvider.complete === 'function') {
423
+ raw = (await this.llmProvider.complete(prompt)).content;
424
+ }
425
+ else if (typeof this.llmProvider.chat === 'function') {
426
+ raw = await this.llmProvider.chat(messagesToLegacyPrompt(prompt));
427
+ }
428
+ else {
429
+ return { encoded: 0, memories: [], skipped: 'llm provider missing completion method' };
430
+ }
164
431
  }
165
432
  catch (err) {
166
433
  this.emit('error', err);
@@ -173,11 +440,11 @@ export class Audrey extends EventEmitter {
173
440
  catch {
174
441
  return { encoded: 0, memories: [], skipped: 'invalid llm response' };
175
442
  }
176
- const memories = parsed.memories ?? [];
443
+ const memories = Array.isArray(parsed.memories)
444
+ ? parsed.memories.map(normalizeReflectionMemory).filter((mem) => mem !== null).slice(0, 50)
445
+ : [];
177
446
  let encoded = 0;
178
447
  for (const mem of memories) {
179
- if (!mem.content || !mem.source)
180
- continue;
181
448
  try {
182
449
  await this.encode({
183
450
  content: mem.content,
@@ -193,32 +460,78 @@ export class Audrey extends EventEmitter {
193
460
  this.emit('error', err);
194
461
  }
195
462
  }
196
- return { encoded, memories: memories };
463
+ return { encoded, memories };
197
464
  }
198
465
  async encodeBatch(paramsList) {
466
+ await this._waitForEmbeddingWarmup();
199
467
  await this._ensureMigrated();
200
- const ids = [];
201
468
  for (const params of paramsList) {
202
- const id = await encodeEpisode(this.db, this.embeddingProvider, params);
469
+ validateEncodeParams(params);
470
+ }
471
+ const normalized = paramsList.map(params => ({
472
+ ...params,
473
+ agent: params.agent ?? this.agent,
474
+ arousalWeight: this.affectConfig.arousalWeight,
475
+ }));
476
+ const vectors = await this.embeddingProvider.embedBatch(normalized.map(params => params.content));
477
+ if (vectors.length !== normalized.length) {
478
+ throw new Error(`embedBatch returned ${vectors.length} vectors for ${normalized.length} inputs`);
479
+ }
480
+ const ids = [];
481
+ const tasks = [];
482
+ for (let i = 0; i < normalized.length; i++) {
483
+ const encodeParams = normalized[i];
484
+ let encodedVector;
485
+ let encodedBuffer;
486
+ const id = await encodeEpisode(this.db, this.embeddingProvider, encodeParams, {
487
+ vector: vectors[i],
488
+ onVector: (vector, buffer) => {
489
+ encodedVector = vector;
490
+ encodedBuffer = buffer;
491
+ },
492
+ });
203
493
  ids.push(id);
204
- this.emit('encode', { id, ...params });
494
+ this.emit('encode', { id, ...paramsList[i] });
495
+ const encodedEmbedding = { vector: encodedVector, buffer: encodedBuffer };
496
+ tasks.push(this._enqueuePostEncode(id, paramsList[i], encodedEmbedding));
205
497
  }
206
- for (let i = 0; i < ids.length; i++) {
207
- this._emitValidation(ids[i], paramsList[i]);
498
+ if (paramsList.some(p => p.waitForConsolidation)) {
499
+ await Promise.all(tasks);
208
500
  }
209
501
  return ids;
210
502
  }
211
503
  async recall(query, options = {}) {
212
- await this._ensureMigrated();
213
- return recallFn(this.db, this.embeddingProvider, query, {
504
+ return this._recallInternal(query, options);
505
+ }
506
+ async recallWithDiagnostics(query, options = {}) {
507
+ const profile = new ProfileRecorder('memory_recall');
508
+ const results = await this._recallInternal(query, options, profile);
509
+ return { results, diagnostics: profile.finish() };
510
+ }
511
+ async _recallInternal(query, options = {}, profile) {
512
+ await this._waitForEmbeddingWarmup(profile, 'recall.wait_for_warmup');
513
+ if (profile)
514
+ await profile.measure('recall.ensure_migrated', () => this._ensureMigrated());
515
+ else
516
+ await this._ensureMigrated();
517
+ const results = await recallFn(this.db, this.embeddingProvider, query, {
214
518
  ...options,
519
+ agent: options.agent ?? this.agent,
520
+ retrieval: options.retrieval ?? this.defaultRetrievalMode,
215
521
  confidenceConfig: this._recallConfig(options),
522
+ profile,
216
523
  });
524
+ this._lastRecallCheckAt = new Date().toISOString();
525
+ this._lastRecallErrors = results.errors ?? [];
526
+ return results;
217
527
  }
218
528
  async *recallStream(query, options = {}) {
529
+ await this._waitForEmbeddingWarmup();
219
530
  await this._ensureMigrated();
220
531
  yield* recallStreamFn(this.db, this.embeddingProvider, query, {
221
532
  ...options,
533
+ agent: options.agent ?? this.agent,
534
+ retrieval: options.retrieval ?? this.defaultRetrievalMode,
222
535
  confidenceConfig: this._recallConfig(options),
223
536
  });
224
537
  }
@@ -234,20 +547,22 @@ export class Audrey extends EventEmitter {
234
547
  }
235
548
  async consolidate(options = {}) {
236
549
  await this._ensureMigrated();
550
+ // Use ?? throughout so 0 / '' are not silently replaced with the default.
237
551
  const result = await runConsolidation(this.db, this.embeddingProvider, {
238
- minClusterSize: options.minClusterSize || this.consolidationConfig.minEpisodes,
239
- similarityThreshold: options.similarityThreshold || 0.80,
552
+ minClusterSize: options.minClusterSize ?? this.consolidationConfig.minEpisodes,
553
+ similarityThreshold: options.similarityThreshold ?? 0.80,
554
+ agent: options.agent ?? this.agent,
240
555
  extractPrinciple: options.extractPrinciple,
241
- llmProvider: options.llmProvider || this.llmProvider || undefined,
556
+ llmProvider: options.llmProvider ?? this.llmProvider ?? undefined,
242
557
  });
243
558
  const run = db_prepare_get_status(this.db, result.runId);
244
- const output = { ...result, status: run?.status || 'completed' };
559
+ const output = { ...result, status: run?.status ?? 'completed' };
245
560
  this.emit('consolidation', output);
246
561
  return output;
247
562
  }
248
563
  decay(options = {}) {
249
564
  const result = applyDecay(this.db, {
250
- dormantThreshold: options.dormantThreshold || this.decayConfig.dormantThreshold,
565
+ dormantThreshold: options.dormantThreshold ?? this.decayConfig.dormantThreshold,
251
566
  halfLives: options.halfLives ?? this.confidenceConfig.halfLives,
252
567
  });
253
568
  this.emit('decay', result);
@@ -354,14 +669,23 @@ export class Audrey extends EventEmitter {
354
669
  device: device ?? null,
355
670
  healthy,
356
671
  reembed_recommended: reembedRecommended,
672
+ pending_consolidation_count: this._pendingPostEncodeIds.size,
673
+ embedding_warm: this._embeddingWarm,
674
+ warmup_duration_ms: this._warmupDurationMs,
675
+ default_retrieval_mode: this.defaultRetrievalMode,
676
+ recall_degraded: this._lastRecallErrors.length > 0,
677
+ last_recall_check_at: this._lastRecallCheckAt,
678
+ last_recall_errors: this._lastRecallErrors,
357
679
  };
358
680
  }
359
- async greeting({ context, recentLimit = 10, principleLimit = 5, identityLimit = 5 } = {}) {
360
- const recent = this.db.prepare('SELECT id, content, source, tags, salience, created_at FROM episodes WHERE "private" = 0 ORDER BY created_at DESC LIMIT ?').all(recentLimit);
361
- const principles = this.db.prepare('SELECT id, content, salience, created_at FROM semantics WHERE state = ? ORDER BY salience DESC LIMIT ?').all('active', principleLimit);
362
- const identity = this.db.prepare('SELECT id, content, tags, salience, created_at FROM episodes WHERE "private" = 1 ORDER BY created_at DESC LIMIT ?').all(identityLimit);
363
- const unresolved = this.db.prepare("SELECT id, content, tags, salience, created_at FROM episodes WHERE tags LIKE '%unresolved%' AND salience > 0.3 ORDER BY created_at DESC LIMIT 10").all();
364
- const rawAffectRows = this.db.prepare("SELECT affect FROM episodes WHERE affect IS NOT NULL AND affect != '{}' ORDER BY created_at DESC LIMIT 20").all();
681
+ async greeting({ context, recentLimit = 10, principleLimit = 5, identityLimit = 5, scope = 'agent' } = {}) {
682
+ const agentClause = scope === 'agent' ? 'AND agent = ?' : '';
683
+ const agentParam = scope === 'agent' ? [this.agent] : [];
684
+ const recent = this.db.prepare(`SELECT id, content, source, tags, salience, created_at FROM episodes WHERE "private" = 0 ${agentClause} ORDER BY created_at DESC LIMIT ?`).all(...agentParam, recentLimit);
685
+ const principles = this.db.prepare(`SELECT id, content, salience, created_at FROM semantics WHERE state = ? ${agentClause} ORDER BY salience DESC LIMIT ?`).all('active', ...agentParam, principleLimit);
686
+ const identity = this.db.prepare(`SELECT id, content, tags, salience, created_at FROM episodes WHERE "private" = 1 ${agentClause} ORDER BY created_at DESC LIMIT ?`).all(...agentParam, identityLimit);
687
+ const unresolved = this.db.prepare(`SELECT id, content, tags, salience, created_at FROM episodes WHERE tags LIKE '%unresolved%' AND salience > 0.3 ${agentClause} ORDER BY created_at DESC LIMIT 10`).all(...agentParam);
688
+ const rawAffectRows = this.db.prepare(`SELECT affect FROM episodes WHERE affect IS NOT NULL AND affect != '{}' ${agentClause} ORDER BY created_at DESC LIMIT 20`).all(...agentParam);
365
689
  const affectParsed = rawAffectRows
366
690
  .map(r => { try {
367
691
  return JSON.parse(r.affect);
@@ -385,7 +709,7 @@ export class Audrey extends EventEmitter {
385
709
  }
386
710
  const result = { recent, principles, mood, unresolved, identity };
387
711
  if (context) {
388
- result.contextual = await this.recall(context, { limit: 5, includePrivate: true });
712
+ result.contextual = await this.recall(context, { limit: 5, includePrivate: true, scope });
389
713
  }
390
714
  return result;
391
715
  }
@@ -436,6 +760,37 @@ export class Audrey extends EventEmitter {
436
760
  suggestConsolidationParams() {
437
761
  return suggestParamsFn(this.db);
438
762
  }
763
+ validate(input) {
764
+ const result = applyFeedback(this.db, input);
765
+ if (result) {
766
+ // Audit row in memory_events so audrey impact can show
767
+ // helpful-vs-wrong breakdown over a window. Outcome is mapped onto the
768
+ // events-table enum: helpful → succeeded, wrong → failed, used → unknown.
769
+ // The original outcome string is preserved in metadata.
770
+ const eventOutcome = input.outcome === 'helpful' ? 'succeeded'
771
+ : input.outcome === 'wrong' ? 'failed'
772
+ : 'unknown';
773
+ insertEvent(this.db, {
774
+ eventType: 'Validate',
775
+ source: 'memory_validate',
776
+ actorAgent: this.agent,
777
+ outcome: eventOutcome,
778
+ redactionState: 'clean',
779
+ metadata: {
780
+ memory_id: result.id,
781
+ memory_type: result.type,
782
+ outcome: input.outcome,
783
+ salience_after: result.salience,
784
+ usage_count_after: result.usageCount,
785
+ },
786
+ });
787
+ this.emit('validate', result);
788
+ }
789
+ return result;
790
+ }
791
+ impact(options = {}) {
792
+ return buildImpactReport(this.db, options.windowDays ?? 7, options.limit ?? 5);
793
+ }
439
794
  forget(id, options = {}) {
440
795
  const result = forgetMemory(this.db, id, options);
441
796
  this.emit('forget', result);
@@ -458,10 +813,28 @@ export class Audrey extends EventEmitter {
458
813
  return;
459
814
  this._closed = true;
460
815
  this.stopAutoConsolidate();
816
+ if (this._pendingPostEncodeIds.size > 0) {
817
+ // Sync close() can't await; emit a clear signal so callers can spot data loss.
818
+ // Use closeAsync() (preferred) or call drainPostEncodeQueue() before close() to avoid this.
819
+ console.error(`[audrey] close() called with ${this._pendingPostEncodeIds.size} pending post-encode tasks ` +
820
+ `(use closeAsync() or await drainPostEncodeQueue() first to avoid losing consolidation work)`);
821
+ }
822
+ closeDatabase(this.db);
823
+ }
824
+ async closeAsync(timeoutMs = 5000) {
825
+ if (this._closed)
826
+ return undefined;
827
+ let result;
828
+ if (this._pendingPostEncodeIds.size > 0) {
829
+ result = await this.drainPostEncodeQueue(timeoutMs);
830
+ }
831
+ this._closed = true;
832
+ this.stopAutoConsolidate();
461
833
  closeDatabase(this.db);
834
+ return result;
462
835
  }
463
836
  async waitForIdle() {
464
- return Promise.resolve();
837
+ await this._postEncodeQueue;
465
838
  }
466
839
  observeTool(input) {
467
840
  const result = observeTool(this.db, {
@@ -511,6 +884,28 @@ export class Audrey extends EventEmitter {
511
884
  });
512
885
  const dryRun = options.dryRun ?? !options.yes;
513
886
  const projectDir = pathResolve(options.projectDir ?? process.cwd());
887
+ // Guard against malicious project_dir from MCP/HTTP callers writing
888
+ // .claude/rules/*.md to arbitrary locations — those files are read by
889
+ // Claude Code on the next session, making this a persistent
890
+ // prompt-injection vector. By default the path must be under cwd or one
891
+ // of the explicit AUDREY_PROMOTE_ROOTS entries.
892
+ if (!dryRun) {
893
+ const allowedRoots = [pathResolve(process.cwd())];
894
+ const extra = process.env.AUDREY_PROMOTE_ROOTS;
895
+ if (extra) {
896
+ for (const root of extra.split(/[:;]/).map(s => s.trim()).filter(Boolean)) {
897
+ allowedRoots.push(pathResolve(root));
898
+ }
899
+ }
900
+ const isUnderAllowedRoot = allowedRoots.some(root => {
901
+ const rel = relative(root, projectDir);
902
+ return rel === '' || (!rel.startsWith('..') && !pathIsAbsolute(rel));
903
+ });
904
+ if (!isUnderAllowedRoot) {
905
+ throw new Error(`promote: refusing to write to ${projectDir} — path is outside cwd and AUDREY_PROMOTE_ROOTS. ` +
906
+ `Set AUDREY_PROMOTE_ROOTS=<path1>:<path2> to allow additional locations.`);
907
+ }
908
+ }
514
909
  const promotedAt = new Date().toISOString();
515
910
  const docs = renderAllRules(candidates, promotedAt);
516
911
  const applied = [];