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.
- package/CHANGELOG.md +191 -0
- package/README.md +216 -117
- package/SECURITY.md +29 -0
- package/dist/mcp-server/config.d.ts +29 -4
- package/dist/mcp-server/config.d.ts.map +1 -1
- package/dist/mcp-server/config.js +100 -17
- package/dist/mcp-server/config.js.map +1 -1
- package/dist/mcp-server/index.d.ts +302 -25
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +1077 -74
- 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 +58 -4
- package/dist/src/audrey.d.ts.map +1 -1
- package/dist/src/audrey.js +469 -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 +8 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -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 +52 -0
- package/dist/src/preflight.d.ts.map +1 -0
- package/dist/src/preflight.js +221 -0
- package/dist/src/preflight.js.map +1 -0
- 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/reflexes.d.ts +35 -0
- package/dist/src/reflexes.d.ts.map +1 -0
- package/dist/src/reflexes.js +87 -0
- package/dist/src/reflexes.js.map +1 -0
- 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 +267 -11
- 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/docs/assets/audrey-feature-grid.jpg +0 -0
- package/docs/assets/audrey-logo.svg +45 -0
- package/docs/assets/audrey-wordmark.png +0 -0
- package/examples/ollama-memory-agent.js +326 -0
- package/package.json +35 -22
- 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/benchmarking.md +0 -151
- package/docs/production-readiness.md +0 -124
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';
|
|
@@ -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
|
|
170
|
+
minEpisodes,
|
|
71
171
|
};
|
|
72
|
-
this.decayConfig = { dormantThreshold
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
246
|
+
};
|
|
247
|
+
if (profile)
|
|
248
|
+
await profile.measure(spanName, wait);
|
|
249
|
+
else
|
|
250
|
+
await wait();
|
|
128
251
|
}
|
|
129
|
-
async
|
|
130
|
-
await this.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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, ...
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
|
237
|
-
similarityThreshold: options.similarityThreshold
|
|
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
|
|
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
|
|
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
|
|
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
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
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);
|
|
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
|
-
|
|
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 = [];
|