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.
- package/CHANGELOG.md +176 -0
- package/README.md +76 -32
- package/SECURITY.md +29 -0
- package/dist/mcp-server/config.d.ts +5 -4
- package/dist/mcp-server/config.d.ts.map +1 -1
- package/dist/mcp-server/config.js +6 -8
- package/dist/mcp-server/config.js.map +1 -1
- package/dist/mcp-server/index.d.ts +219 -23
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +681 -95
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/adaptive.d.ts.map +1 -1
- package/dist/src/adaptive.js +3 -1
- package/dist/src/adaptive.js.map +1 -1
- package/dist/src/affect.d.ts +4 -1
- package/dist/src/affect.d.ts.map +1 -1
- package/dist/src/affect.js +6 -4
- package/dist/src/affect.js.map +1 -1
- package/dist/src/audrey.d.ts +54 -4
- package/dist/src/audrey.d.ts.map +1 -1
- package/dist/src/audrey.js +457 -62
- package/dist/src/audrey.js.map +1 -1
- package/dist/src/capsule.d.ts +2 -1
- package/dist/src/capsule.d.ts.map +1 -1
- package/dist/src/capsule.js +14 -4
- package/dist/src/capsule.js.map +1 -1
- package/dist/src/causal.d.ts.map +1 -1
- package/dist/src/causal.js +20 -2
- package/dist/src/causal.js.map +1 -1
- package/dist/src/confidence.d.ts.map +1 -1
- package/dist/src/confidence.js +3 -0
- package/dist/src/confidence.js.map +1 -1
- package/dist/src/consolidate.d.ts +1 -0
- package/dist/src/consolidate.d.ts.map +1 -1
- package/dist/src/consolidate.js +35 -19
- package/dist/src/consolidate.js.map +1 -1
- package/dist/src/controller.d.ts +38 -0
- package/dist/src/controller.d.ts.map +1 -0
- package/dist/src/controller.js +169 -0
- package/dist/src/controller.js.map +1 -0
- package/dist/src/db.d.ts.map +1 -1
- package/dist/src/db.js +12 -0
- package/dist/src/db.js.map +1 -1
- package/dist/src/decay.d.ts.map +1 -1
- package/dist/src/decay.js +57 -50
- package/dist/src/decay.js.map +1 -1
- package/dist/src/embedding.d.ts.map +1 -1
- package/dist/src/embedding.js +31 -3
- package/dist/src/embedding.js.map +1 -1
- package/dist/src/encode.d.ts +9 -2
- package/dist/src/encode.d.ts.map +1 -1
- package/dist/src/encode.js +21 -8
- package/dist/src/encode.js.map +1 -1
- package/dist/src/export.d.ts.map +1 -1
- package/dist/src/export.js +5 -3
- package/dist/src/export.js.map +1 -1
- package/dist/src/feedback.d.ts +29 -0
- package/dist/src/feedback.d.ts.map +1 -0
- package/dist/src/feedback.js +123 -0
- package/dist/src/feedback.js.map +1 -0
- package/dist/src/forget.d.ts.map +1 -1
- package/dist/src/forget.js +58 -50
- package/dist/src/forget.js.map +1 -1
- package/dist/src/fts.js +1 -1
- package/dist/src/fts.js.map +1 -1
- package/dist/src/hybrid-recall.d.ts +2 -1
- package/dist/src/hybrid-recall.d.ts.map +1 -1
- package/dist/src/hybrid-recall.js +35 -26
- package/dist/src/hybrid-recall.js.map +1 -1
- package/dist/src/impact.d.ts +47 -0
- package/dist/src/impact.d.ts.map +1 -0
- package/dist/src/impact.js +146 -0
- package/dist/src/impact.js.map +1 -0
- package/dist/src/import.d.ts +177 -1
- package/dist/src/import.d.ts.map +1 -1
- package/dist/src/import.js +206 -17
- package/dist/src/import.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/interference.d.ts +5 -2
- package/dist/src/interference.d.ts.map +1 -1
- package/dist/src/interference.js +27 -20
- package/dist/src/interference.js.map +1 -1
- package/dist/src/llm.d.ts.map +1 -1
- package/dist/src/llm.js +1 -0
- package/dist/src/llm.js.map +1 -1
- package/dist/src/migrate.d.ts.map +1 -1
- package/dist/src/migrate.js +21 -9
- package/dist/src/migrate.js.map +1 -1
- package/dist/src/preflight.d.ts +2 -1
- package/dist/src/preflight.d.ts.map +1 -1
- package/dist/src/preflight.js +25 -5
- package/dist/src/preflight.js.map +1 -1
- package/dist/src/profile.d.ts +23 -0
- package/dist/src/profile.d.ts.map +1 -0
- package/dist/src/profile.js +51 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/promote.d.ts.map +1 -1
- package/dist/src/promote.js +2 -3
- package/dist/src/promote.js.map +1 -1
- package/dist/src/prompts.d.ts.map +1 -1
- package/dist/src/prompts.js +76 -47
- package/dist/src/prompts.js.map +1 -1
- package/dist/src/recall.d.ts +9 -6
- package/dist/src/recall.d.ts.map +1 -1
- package/dist/src/recall.js +182 -40
- package/dist/src/recall.js.map +1 -1
- package/dist/src/redact.d.ts +7 -1
- package/dist/src/redact.d.ts.map +1 -1
- package/dist/src/redact.js +94 -11
- package/dist/src/redact.js.map +1 -1
- package/dist/src/rollback.d.ts.map +1 -1
- package/dist/src/rollback.js +9 -4
- package/dist/src/rollback.js.map +1 -1
- package/dist/src/routes.d.ts +1 -0
- package/dist/src/routes.d.ts.map +1 -1
- package/dist/src/routes.js +185 -6
- package/dist/src/routes.js.map +1 -1
- package/dist/src/rules-compiler.d.ts.map +1 -1
- package/dist/src/rules-compiler.js +36 -6
- package/dist/src/rules-compiler.js.map +1 -1
- package/dist/src/server.d.ts +2 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +42 -4
- package/dist/src/server.js.map +1 -1
- package/dist/src/tool-trace.d.ts.map +1 -1
- package/dist/src/tool-trace.js +42 -29
- package/dist/src/tool-trace.js.map +1 -1
- package/dist/src/types.d.ts +28 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/ulid.d.ts.map +1 -1
- package/dist/src/ulid.js +52 -2
- package/dist/src/ulid.js.map +1 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +8 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/src/validate.d.ts +2 -0
- package/dist/src/validate.d.ts.map +1 -1
- package/dist/src/validate.js +60 -29
- package/dist/src/validate.js.map +1 -1
- package/package.json +20 -26
- package/docs/assets/benchmarks/local-benchmark.svg +0 -45
- package/docs/assets/benchmarks/operations-benchmark.svg +0 -45
- package/docs/assets/benchmarks/published-memory-standards.svg +0 -50
- package/docs/audrey-for-dummies.md +0 -670
- package/docs/benchmarking.md +0 -151
- package/docs/future-of-llm-memory.md +0 -452
- package/docs/mcp-hosts.md +0 -206
- package/docs/ollama-local-agents.md +0 -128
- package/docs/production-readiness.md +0 -128
package/dist/src/audrey.js
CHANGED
|
@@ -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
|
|
170
|
+
minEpisodes,
|
|
73
171
|
};
|
|
74
|
-
this.decayConfig = { dormantThreshold
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
246
|
+
};
|
|
247
|
+
if (profile)
|
|
248
|
+
await profile.measure(spanName, wait);
|
|
249
|
+
else
|
|
250
|
+
await wait();
|
|
130
251
|
}
|
|
131
|
-
async
|
|
132
|
-
await this.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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, ...
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
|
239
|
-
similarityThreshold: options.similarityThreshold
|
|
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
|
|
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
|
|
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
|
|
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
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
const
|
|
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
|
-
|
|
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 = [];
|