audrey 0.20.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 (156) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +216 -117
  3. package/SECURITY.md +29 -0
  4. package/dist/mcp-server/config.d.ts +29 -4
  5. package/dist/mcp-server/config.d.ts.map +1 -1
  6. package/dist/mcp-server/config.js +100 -17
  7. package/dist/mcp-server/config.js.map +1 -1
  8. package/dist/mcp-server/index.d.ts +302 -25
  9. package/dist/mcp-server/index.d.ts.map +1 -1
  10. package/dist/mcp-server/index.js +1077 -74
  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 +58 -4
  20. package/dist/src/audrey.d.ts.map +1 -1
  21. package/dist/src/audrey.js +469 -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 +8 -0
  79. package/dist/src/index.d.ts.map +1 -1
  80. package/dist/src/index.js +4 -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 +52 -0
  93. package/dist/src/preflight.d.ts.map +1 -0
  94. package/dist/src/preflight.js +221 -0
  95. package/dist/src/preflight.js.map +1 -0
  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/reflexes.d.ts +35 -0
  115. package/dist/src/reflexes.d.ts.map +1 -0
  116. package/dist/src/reflexes.js +87 -0
  117. package/dist/src/reflexes.js.map +1 -0
  118. package/dist/src/rollback.d.ts.map +1 -1
  119. package/dist/src/rollback.js +9 -4
  120. package/dist/src/rollback.js.map +1 -1
  121. package/dist/src/routes.d.ts +1 -0
  122. package/dist/src/routes.d.ts.map +1 -1
  123. package/dist/src/routes.js +267 -11
  124. package/dist/src/routes.js.map +1 -1
  125. package/dist/src/rules-compiler.d.ts.map +1 -1
  126. package/dist/src/rules-compiler.js +36 -6
  127. package/dist/src/rules-compiler.js.map +1 -1
  128. package/dist/src/server.d.ts +2 -1
  129. package/dist/src/server.d.ts.map +1 -1
  130. package/dist/src/server.js +42 -4
  131. package/dist/src/server.js.map +1 -1
  132. package/dist/src/tool-trace.d.ts.map +1 -1
  133. package/dist/src/tool-trace.js +42 -29
  134. package/dist/src/tool-trace.js.map +1 -1
  135. package/dist/src/types.d.ts +28 -1
  136. package/dist/src/types.d.ts.map +1 -1
  137. package/dist/src/ulid.d.ts.map +1 -1
  138. package/dist/src/ulid.js +52 -2
  139. package/dist/src/ulid.js.map +1 -1
  140. package/dist/src/utils.d.ts.map +1 -1
  141. package/dist/src/utils.js +8 -1
  142. package/dist/src/utils.js.map +1 -1
  143. package/dist/src/validate.d.ts +2 -0
  144. package/dist/src/validate.d.ts.map +1 -1
  145. package/dist/src/validate.js +60 -29
  146. package/dist/src/validate.js.map +1 -1
  147. package/docs/assets/audrey-feature-grid.jpg +0 -0
  148. package/docs/assets/audrey-logo.svg +45 -0
  149. package/docs/assets/audrey-wordmark.png +0 -0
  150. package/examples/ollama-memory-agent.js +326 -0
  151. package/package.json +35 -22
  152. package/docs/assets/benchmarks/local-benchmark.svg +0 -45
  153. package/docs/assets/benchmarks/operations-benchmark.svg +0 -45
  154. package/docs/assets/benchmarks/published-memory-standards.svg +0 -50
  155. package/docs/benchmarking.md +0 -151
  156. package/docs/production-readiness.md +0 -124
@@ -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';
@@ -20,11 +22,101 @@ import { detectResonance } from './affect.js';
20
22
  import { observeTool } from './tool-trace.js';
21
23
  import { listEvents, countEvents, recentFailures, } from './events.js';
22
24
  import { buildCapsule } from './capsule.js';
25
+ import { buildPreflight } from './preflight.js';
26
+ import { buildReflexReport } from './reflexes.js';
23
27
  import { findPromotionCandidates, } from './promote.js';
24
28
  import { renderAllRules } from './rules-compiler.js';
25
29
  import { insertEvent } from './events.js';
26
30
  import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
27
- 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
+ }
28
120
  export class Audrey extends EventEmitter {
29
121
  agent;
30
122
  dataDir;
@@ -37,10 +129,18 @@ export class Audrey extends EventEmitter {
37
129
  interferenceConfig;
38
130
  contextConfig;
39
131
  affectConfig;
132
+ defaultRetrievalMode;
40
133
  autoReflect;
41
134
  _migrationPending;
42
135
  _autoConsolidateTimer;
43
136
  _closed;
137
+ _postEncodeQueue;
138
+ _pendingPostEncodeIds;
139
+ _embeddingWarm;
140
+ _embeddingWarmupPromise;
141
+ _warmupDurationMs;
142
+ _lastRecallCheckAt;
143
+ _lastRecallErrors;
44
144
  constructor({ dataDir = './audrey-data', agent = 'default', embedding = { provider: 'mock', dimensions: 64 }, llm, confidence = {}, consolidation = {}, decay = {}, interference = {}, context = {}, affect = {}, autoReflect = false, } = {}) {
45
145
  super();
46
146
  const dormantThreshold = decay.dormantThreshold ?? 0.1;
@@ -67,9 +167,9 @@ export class Audrey extends EventEmitter {
67
167
  affectWeight: affect.weight ?? 0.2,
68
168
  };
69
169
  this.consolidationConfig = {
70
- minEpisodes: consolidation.minEpisodes || 3,
170
+ minEpisodes,
71
171
  };
72
- this.decayConfig = { dormantThreshold: decay.dormantThreshold || 0.1 };
172
+ this.decayConfig = { dormantThreshold };
73
173
  this._autoConsolidateTimer = null;
74
174
  this._closed = false;
75
175
  this.interferenceConfig = {
@@ -93,7 +193,15 @@ export class Audrey extends EventEmitter {
93
193
  affectThreshold: affect.resonance?.affectThreshold ?? 0.6,
94
194
  },
95
195
  };
196
+ this.defaultRetrievalMode = 'hybrid';
96
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 = [];
97
205
  }
98
206
  async _ensureMigrated() {
99
207
  if (!this._migrationPending)
@@ -102,54 +210,207 @@ export class Audrey extends EventEmitter {
102
210
  this._migrationPending = false;
103
211
  this.emit('migration', counts);
104
212
  }
105
- _emitValidation(id, params) {
106
- validateMemory(this.db, this.embeddingProvider, { id, ...params }, {
107
- 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;
108
229
  })
109
- .then(validation => {
110
- if (validation.action === 'reinforced') {
111
- this.emit('reinforcement', {
112
- episodeId: id,
113
- targetId: validation.semanticId,
114
- similarity: validation.similarity,
115
- });
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;
116
241
  }
117
- else if (validation.action === 'contradiction') {
118
- this.emit('contradiction', {
119
- episodeId: id,
120
- contradictionId: validation.contradictionId,
121
- semanticId: validation.semanticId,
122
- similarity: validation.similarity,
123
- resolution: validation.resolution,
124
- });
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.
125
245
  }
126
- })
127
- .catch(err => this.emit('error', err));
246
+ };
247
+ if (profile)
248
+ await profile.measure(spanName, wait);
249
+ else
250
+ await wait();
128
251
  }
129
- async encode(params) {
130
- await this._ensureMigrated();
131
- const encodeParams = { ...params, arousalWeight: this.affectConfig.arousalWeight };
132
- const id = await encodeEpisode(this.db, this.embeddingProvider, encodeParams);
133
- 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) {
134
286
  if (this.interferenceConfig.enabled) {
135
- applyInterference(this.db, this.embeddingProvider, id, params, this.interferenceConfig)
136
- .then(affected => {
287
+ await this._runPostEncodeStage('interference', async () => {
288
+ const affected = await applyInterference(this.db, this.embeddingProvider, id, params, this.interferenceConfig, embedding);
137
289
  if (affected.length > 0) {
138
290
  this.emit('interference', { episodeId: id, affected });
139
291
  }
140
- })
141
- .catch(err => this.emit('error', err));
292
+ });
142
293
  }
143
294
  if (this.affectConfig.enabled && this.affectConfig.resonance.enabled && params.affect?.valence !== undefined) {
144
- detectResonance(this.db, this.embeddingProvider, id, params, this.affectConfig.resonance)
145
- .then(echoes => {
295
+ await this._runPostEncodeStage('resonance', async () => {
296
+ const echoes = await detectResonance(this.db, this.embeddingProvider, id, params, this.affectConfig.resonance, embedding);
146
297
  if (echoes.length > 0) {
147
298
  this.emit('resonance', { episodeId: id, affect: params.affect, echoes });
148
299
  }
149
- })
150
- .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;
151
413
  }
152
- this._emitValidation(id, params);
153
414
  return id;
154
415
  }
155
416
  async reflect(turns) {
@@ -158,7 +419,15 @@ export class Audrey extends EventEmitter {
158
419
  const prompt = buildReflectionPrompt(turns);
159
420
  let raw;
160
421
  try {
161
- 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
+ }
162
431
  }
163
432
  catch (err) {
164
433
  this.emit('error', err);
@@ -171,11 +440,11 @@ export class Audrey extends EventEmitter {
171
440
  catch {
172
441
  return { encoded: 0, memories: [], skipped: 'invalid llm response' };
173
442
  }
174
- const memories = parsed.memories ?? [];
443
+ const memories = Array.isArray(parsed.memories)
444
+ ? parsed.memories.map(normalizeReflectionMemory).filter((mem) => mem !== null).slice(0, 50)
445
+ : [];
175
446
  let encoded = 0;
176
447
  for (const mem of memories) {
177
- if (!mem.content || !mem.source)
178
- continue;
179
448
  try {
180
449
  await this.encode({
181
450
  content: mem.content,
@@ -191,32 +460,78 @@ export class Audrey extends EventEmitter {
191
460
  this.emit('error', err);
192
461
  }
193
462
  }
194
- return { encoded, memories: memories };
463
+ return { encoded, memories };
195
464
  }
196
465
  async encodeBatch(paramsList) {
466
+ await this._waitForEmbeddingWarmup();
197
467
  await this._ensureMigrated();
198
- const ids = [];
199
468
  for (const params of paramsList) {
200
- 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
+ });
201
493
  ids.push(id);
202
- 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));
203
497
  }
204
- for (let i = 0; i < ids.length; i++) {
205
- this._emitValidation(ids[i], paramsList[i]);
498
+ if (paramsList.some(p => p.waitForConsolidation)) {
499
+ await Promise.all(tasks);
206
500
  }
207
501
  return ids;
208
502
  }
209
503
  async recall(query, options = {}) {
210
- await this._ensureMigrated();
211
- 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, {
212
518
  ...options,
519
+ agent: options.agent ?? this.agent,
520
+ retrieval: options.retrieval ?? this.defaultRetrievalMode,
213
521
  confidenceConfig: this._recallConfig(options),
522
+ profile,
214
523
  });
524
+ this._lastRecallCheckAt = new Date().toISOString();
525
+ this._lastRecallErrors = results.errors ?? [];
526
+ return results;
215
527
  }
216
528
  async *recallStream(query, options = {}) {
529
+ await this._waitForEmbeddingWarmup();
217
530
  await this._ensureMigrated();
218
531
  yield* recallStreamFn(this.db, this.embeddingProvider, query, {
219
532
  ...options,
533
+ agent: options.agent ?? this.agent,
534
+ retrieval: options.retrieval ?? this.defaultRetrievalMode,
220
535
  confidenceConfig: this._recallConfig(options),
221
536
  });
222
537
  }
@@ -232,20 +547,22 @@ export class Audrey extends EventEmitter {
232
547
  }
233
548
  async consolidate(options = {}) {
234
549
  await this._ensureMigrated();
550
+ // Use ?? throughout so 0 / '' are not silently replaced with the default.
235
551
  const result = await runConsolidation(this.db, this.embeddingProvider, {
236
- minClusterSize: options.minClusterSize || this.consolidationConfig.minEpisodes,
237
- similarityThreshold: options.similarityThreshold || 0.80,
552
+ minClusterSize: options.minClusterSize ?? this.consolidationConfig.minEpisodes,
553
+ similarityThreshold: options.similarityThreshold ?? 0.80,
554
+ agent: options.agent ?? this.agent,
238
555
  extractPrinciple: options.extractPrinciple,
239
- llmProvider: options.llmProvider || this.llmProvider || undefined,
556
+ llmProvider: options.llmProvider ?? this.llmProvider ?? undefined,
240
557
  });
241
558
  const run = db_prepare_get_status(this.db, result.runId);
242
- const output = { ...result, status: run?.status || 'completed' };
559
+ const output = { ...result, status: run?.status ?? 'completed' };
243
560
  this.emit('consolidation', output);
244
561
  return output;
245
562
  }
246
563
  decay(options = {}) {
247
564
  const result = applyDecay(this.db, {
248
- dormantThreshold: options.dormantThreshold || this.decayConfig.dormantThreshold,
565
+ dormantThreshold: options.dormantThreshold ?? this.decayConfig.dormantThreshold,
249
566
  halfLives: options.halfLives ?? this.confidenceConfig.halfLives,
250
567
  });
251
568
  this.emit('decay', result);
@@ -352,14 +669,23 @@ export class Audrey extends EventEmitter {
352
669
  device: device ?? null,
353
670
  healthy,
354
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,
355
679
  };
356
680
  }
357
- async greeting({ context, recentLimit = 10, principleLimit = 5, identityLimit = 5 } = {}) {
358
- 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);
359
- const principles = this.db.prepare('SELECT id, content, salience, created_at FROM semantics WHERE state = ? ORDER BY salience DESC LIMIT ?').all('active', principleLimit);
360
- 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);
361
- 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();
362
- 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);
363
689
  const affectParsed = rawAffectRows
364
690
  .map(r => { try {
365
691
  return JSON.parse(r.affect);
@@ -383,7 +709,7 @@ export class Audrey extends EventEmitter {
383
709
  }
384
710
  const result = { recent, principles, mood, unresolved, identity };
385
711
  if (context) {
386
- result.contextual = await this.recall(context, { limit: 5, includePrivate: true });
712
+ result.contextual = await this.recall(context, { limit: 5, includePrivate: true, scope });
387
713
  }
388
714
  return result;
389
715
  }
@@ -434,6 +760,37 @@ export class Audrey extends EventEmitter {
434
760
  suggestConsolidationParams() {
435
761
  return suggestParamsFn(this.db);
436
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
+ }
437
794
  forget(id, options = {}) {
438
795
  const result = forgetMemory(this.db, id, options);
439
796
  this.emit('forget', result);
@@ -456,10 +813,28 @@ export class Audrey extends EventEmitter {
456
813
  return;
457
814
  this._closed = true;
458
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
+ }
459
822
  closeDatabase(this.db);
460
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();
833
+ closeDatabase(this.db);
834
+ return result;
835
+ }
461
836
  async waitForIdle() {
462
- return Promise.resolve();
837
+ await this._postEncodeQueue;
463
838
  }
464
839
  observeTool(input) {
465
840
  const result = observeTool(this.db, {
@@ -483,6 +858,16 @@ export class Audrey extends EventEmitter {
483
858
  this.emit('capsule', capsule);
484
859
  return capsule;
485
860
  }
861
+ async preflight(action, options = {}) {
862
+ const preflight = await buildPreflight(this, action, options);
863
+ this.emit('preflight', preflight);
864
+ return preflight;
865
+ }
866
+ async reflexes(action, options = {}) {
867
+ const report = await buildReflexReport(this, action, options);
868
+ this.emit('reflexes', report);
869
+ return report;
870
+ }
486
871
  findPromotionCandidates(options = {}) {
487
872
  return findPromotionCandidates(this.db, options);
488
873
  }
@@ -499,6 +884,28 @@ export class Audrey extends EventEmitter {
499
884
  });
500
885
  const dryRun = options.dryRun ?? !options.yes;
501
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
+ }
502
909
  const promotedAt = new Date().toISOString();
503
910
  const docs = renderAllRules(candidates, promotedAt);
504
911
  const applied = [];