audrey 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { join } from 'node:path';
3
3
 
4
- export const VERSION = '0.14.0';
4
+ export const VERSION = '0.15.0';
5
5
  export const SERVER_NAME = 'audrey-memory';
6
6
  export const DEFAULT_DATA_DIR = join(homedir(), '.audrey', 'data');
7
7
 
@@ -63,11 +63,8 @@ function install() {
63
63
  process.exit(1);
64
64
  }
65
65
 
66
- if (process.env.OPENAI_API_KEY) {
67
- console.log('Detected OPENAI_API_KEY — using OpenAI embeddings (1536d)');
68
- } else {
69
- console.log('No OPENAI_API_KEY found — using mock embeddings (upgrade anytime by re-running with the key set)');
70
- }
66
+ const embedding = resolveEmbeddingProvider(process.env);
67
+ console.log(`Embedding: ${embedding.provider} (${embedding.dimensions}d)`);
71
68
 
72
69
  if (process.env.ANTHROPIC_API_KEY) {
73
70
  console.log('Detected ANTHROPIC_API_KEY — enabling LLM-powered consolidation + contradiction detection');
@@ -199,11 +196,12 @@ async function main() {
199
196
  arousal: z.number().min(0).max(1).optional().describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
200
197
  label: z.string().optional().describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
201
198
  }).optional().describe('Emotional affect — how this memory feels'),
202
- private: z.boolean().optional().describe('If true, memory is only visible to the AI excluded from public recall results'),
199
+ private: z.boolean().optional().describe('If true, memory is only visible to the AI excluded from public recall results'),
200
+ auto_supersede: z.boolean().optional().describe('If true, automatically supersede the most similar existing memory if similarity > 0.95'),
203
201
  },
204
- async ({ content, source, tags, salience, private: isPrivate, context, affect }) => {
202
+ async ({ content, source, tags, salience, private: isPrivate, context, affect, auto_supersede }) => {
205
203
  try {
206
- const id = await audrey.encode({ content, source, tags, salience, private: isPrivate, context, affect });
204
+ const id = await audrey.encode({ content, source, tags, salience, private: isPrivate, context, affect, autoSupersede: auto_supersede });
207
205
  return toolResult({ id, content, source, private: isPrivate ?? false });
208
206
  } catch (err) {
209
207
  return toolError(err);
@@ -411,6 +409,27 @@ async function main() {
411
409
  },
412
410
  );
413
411
 
412
+ server.tool(
413
+ 'memory_dream',
414
+ {
415
+ min_cluster_size: z.number().optional().describe('Minimum episodes per cluster for consolidation'),
416
+ similarity_threshold: z.number().optional().describe('Similarity threshold for clustering'),
417
+ dormant_threshold: z.number().min(0).max(1).optional().describe('Confidence below which memories go dormant'),
418
+ },
419
+ async ({ min_cluster_size, similarity_threshold, dormant_threshold }) => {
420
+ try {
421
+ const result = await audrey.dream({
422
+ minClusterSize: min_cluster_size,
423
+ similarityThreshold: similarity_threshold,
424
+ dormantThreshold: dormant_threshold,
425
+ });
426
+ return toolResult(result);
427
+ } catch (err) {
428
+ return toolError(err);
429
+ }
430
+ },
431
+ );
432
+
414
433
  server.tool(
415
434
  'memory_greeting',
416
435
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audrey",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Biological memory architecture for AI agents — encode, consolidate, and recall memories with confidence decay, contradiction detection, and causal graphs",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/audrey.js CHANGED
@@ -193,7 +193,10 @@ export class Audrey extends EventEmitter {
193
193
  */
194
194
  async encode(params) {
195
195
  await this._ensureMigrated();
196
- const encodeParams = { ...params, arousalWeight: this.affectConfig.arousalWeight };
196
+ const encodeParams = {
197
+ ...params,
198
+ arousalWeight: this.affectConfig.arousalWeight,
199
+ };
197
200
  const id = await encodeEpisode(this.db, this.embeddingProvider, encodeParams);
198
201
  this.emit('encode', { id, ...params });
199
202
  if (this.interferenceConfig.enabled) {
@@ -503,7 +506,47 @@ export class Audrey extends EventEmitter {
503
506
  };
504
507
  }
505
508
 
506
- const result = { recent, principles, mood, unresolved, identity };
509
+ // Health & staleness
510
+ const stats = this.introspect();
511
+ const status = this.memoryStatus();
512
+
513
+ const lastConsolidation = stats.lastConsolidation;
514
+ const daysSinceConsolidation = lastConsolidation
515
+ ? (Date.now() - new Date(lastConsolidation).getTime()) / (1000 * 60 * 60 * 24)
516
+ : null;
517
+
518
+ const lastEpisode = this.db.prepare(
519
+ 'SELECT created_at FROM episodes ORDER BY created_at DESC LIMIT 1'
520
+ ).get();
521
+ const daysSinceLastMemory = lastEpisode
522
+ ? (Date.now() - new Date(lastEpisode.created_at).getTime()) / (1000 * 60 * 60 * 24)
523
+ : null;
524
+
525
+ const suggestions = [];
526
+ if (stats.episodic > 50 && stats.totalConsolidationRuns === 0) {
527
+ suggestions.push('run consolidation — enough episodes have accumulated');
528
+ }
529
+ if (daysSinceConsolidation !== null && daysSinceConsolidation > 7) {
530
+ suggestions.push('consolidation overdue — consider running dream()');
531
+ }
532
+ if (!status.healthy) {
533
+ suggestions.push('vector tables out of sync — run reembed');
534
+ }
535
+
536
+ const health = {
537
+ totalEpisodes: stats.episodic,
538
+ totalSemantics: stats.semantic,
539
+ totalProcedural: stats.procedural,
540
+ dormant: stats.dormant,
541
+ contradictions: stats.contradictions.open,
542
+ consolidationRuns: stats.totalConsolidationRuns,
543
+ lastConsolidation,
544
+ healthy: status.healthy,
545
+ stale: daysSinceLastMemory !== null && daysSinceLastMemory > 7,
546
+ suggestion: suggestions.length > 0 ? suggestions.join('; ') : null,
547
+ };
548
+
549
+ const result = { recent, principles, mood, unresolved, identity, health };
507
550
 
508
551
  if (context) {
509
552
  result.contextual = await this.recall(context, { limit: 5, includePrivate: true });
@@ -512,6 +555,40 @@ export class Audrey extends EventEmitter {
512
555
  return result;
513
556
  }
514
557
 
558
+ /**
559
+ * Run a full "dreaming" cycle: consolidation + decay + interference cleanup.
560
+ * Inspired by CMA paper's REM-like replay. Designed to be called by hooks or on a schedule.
561
+ * @param {{ minClusterSize?: number, similarityThreshold?: number, dormantThreshold?: number }} [options]
562
+ * @returns {Promise<{ consolidated: ConsolidationResult, decayed: { totalEvaluated: number, transitionedToDormant: number }, summary: string }>}
563
+ */
564
+ async dream(options = {}) {
565
+ await this._ensureMigrated();
566
+
567
+ const consolidated = await this.consolidate({
568
+ minClusterSize: options.minClusterSize,
569
+ similarityThreshold: options.similarityThreshold,
570
+ });
571
+
572
+ const decayed = this.decay({
573
+ dormantThreshold: options.dormantThreshold,
574
+ });
575
+
576
+ const parts = [];
577
+ if (consolidated.principlesExtracted > 0) {
578
+ parts.push(`extracted ${consolidated.principlesExtracted} principles from ${consolidated.clustersFound} clusters`);
579
+ }
580
+ if (decayed.transitionedToDormant > 0) {
581
+ parts.push(`${decayed.transitionedToDormant} memories went dormant`);
582
+ }
583
+ if (parts.length === 0) {
584
+ parts.push('nothing changed — memories are well-maintained');
585
+ }
586
+
587
+ const result = { consolidated, decayed, summary: parts.join('; ') };
588
+ this.emit('dream', result);
589
+ return result;
590
+ }
591
+
515
592
  export() {
516
593
  return exportMemories(this.db);
517
594
  }
package/src/db.js CHANGED
@@ -1,333 +1,333 @@
1
- import Database from 'better-sqlite3';
2
- import * as sqliteVec from 'sqlite-vec';
3
- import { join } from 'node:path';
4
- import { mkdirSync, existsSync } from 'node:fs';
5
-
6
- const SCHEMA = `
7
- CREATE TABLE IF NOT EXISTS episodes (
8
- id TEXT PRIMARY KEY,
9
- content TEXT NOT NULL,
10
- embedding BLOB,
11
- source TEXT NOT NULL CHECK(source IN ('direct-observation','told-by-user','tool-result','inference','model-generated')),
12
- source_reliability REAL NOT NULL,
13
- salience REAL DEFAULT 0.5,
14
- context TEXT DEFAULT '{}',
15
- affect TEXT DEFAULT '{}',
16
- tags TEXT,
17
- causal_trigger TEXT,
18
- causal_consequence TEXT,
19
- created_at TEXT NOT NULL,
20
- embedding_model TEXT,
21
- embedding_version TEXT,
22
- supersedes TEXT,
23
- superseded_by TEXT,
24
- consolidated INTEGER DEFAULT 0,
25
- private INTEGER DEFAULT 0,
26
- FOREIGN KEY (supersedes) REFERENCES episodes(id)
27
- );
28
-
29
- CREATE TABLE IF NOT EXISTS semantics (
30
- id TEXT PRIMARY KEY,
31
- content TEXT NOT NULL,
32
- embedding BLOB,
33
- state TEXT DEFAULT 'active' CHECK(state IN ('active','disputed','superseded','context_dependent','dormant','rolled_back')),
34
- conditions TEXT,
35
- evidence_episode_ids TEXT,
36
- evidence_count INTEGER DEFAULT 0,
37
- supporting_count INTEGER DEFAULT 0,
38
- contradicting_count INTEGER DEFAULT 0,
39
- source_type_diversity INTEGER DEFAULT 0,
40
- consolidation_checkpoint TEXT,
41
- embedding_model TEXT,
42
- embedding_version TEXT,
43
- consolidation_model TEXT,
44
- consolidation_prompt_hash TEXT,
45
- created_at TEXT NOT NULL,
46
- last_reinforced_at TEXT,
47
- retrieval_count INTEGER DEFAULT 0,
48
- challenge_count INTEGER DEFAULT 0,
49
- interference_count INTEGER DEFAULT 0,
50
- salience REAL DEFAULT 0.5
51
- );
52
-
53
- CREATE TABLE IF NOT EXISTS procedures (
54
- id TEXT PRIMARY KEY,
55
- content TEXT NOT NULL,
56
- embedding BLOB,
57
- state TEXT DEFAULT 'active' CHECK(state IN ('active','disputed','superseded','context_dependent','dormant','rolled_back')),
58
- trigger_conditions TEXT,
59
- evidence_episode_ids TEXT,
60
- success_count INTEGER DEFAULT 0,
61
- failure_count INTEGER DEFAULT 0,
62
- embedding_model TEXT,
63
- embedding_version TEXT,
64
- created_at TEXT NOT NULL,
65
- last_reinforced_at TEXT,
66
- retrieval_count INTEGER DEFAULT 0,
67
- interference_count INTEGER DEFAULT 0,
68
- salience REAL DEFAULT 0.5
69
- );
70
-
71
- CREATE TABLE IF NOT EXISTS causal_links (
72
- id TEXT PRIMARY KEY,
73
- cause_id TEXT NOT NULL,
74
- effect_id TEXT NOT NULL,
75
- link_type TEXT DEFAULT 'causal' CHECK(link_type IN ('causal','correlational','temporal')),
76
- mechanism TEXT,
77
- confidence REAL,
78
- evidence_count INTEGER DEFAULT 1,
79
- created_at TEXT NOT NULL
80
- );
81
-
82
- CREATE TABLE IF NOT EXISTS contradictions (
83
- id TEXT PRIMARY KEY,
84
- claim_a_id TEXT NOT NULL,
85
- claim_b_id TEXT NOT NULL,
86
- claim_a_type TEXT NOT NULL,
87
- claim_b_type TEXT NOT NULL,
88
- state TEXT DEFAULT 'open' CHECK(state IN ('open','resolved','context_dependent','reopened')),
89
- resolution TEXT,
90
- resolved_at TEXT,
91
- reopened_at TEXT,
92
- reopen_evidence_id TEXT,
93
- created_at TEXT NOT NULL
94
- );
95
-
96
- CREATE TABLE IF NOT EXISTS consolidation_runs (
97
- id TEXT PRIMARY KEY,
98
- checkpoint_cursor TEXT,
99
- input_episode_ids TEXT,
100
- output_memory_ids TEXT,
101
- confidence_deltas TEXT,
102
- consolidation_model TEXT,
103
- consolidation_prompt_hash TEXT,
104
- started_at TEXT,
105
- completed_at TEXT,
106
- status TEXT DEFAULT 'running' CHECK(status IN ('running','completed','failed','rolled_back'))
107
- );
108
-
109
- CREATE TABLE IF NOT EXISTS audrey_config (
110
- key TEXT PRIMARY KEY,
111
- value TEXT NOT NULL
112
- );
113
-
114
- CREATE TABLE IF NOT EXISTS consolidation_metrics (
115
- id TEXT PRIMARY KEY,
116
- run_id TEXT NOT NULL,
117
- min_cluster_size INTEGER NOT NULL,
118
- similarity_threshold REAL NOT NULL,
119
- episodes_evaluated INTEGER NOT NULL,
120
- clusters_found INTEGER NOT NULL,
121
- principles_extracted INTEGER NOT NULL,
122
- created_at TEXT NOT NULL,
123
- FOREIGN KEY (run_id) REFERENCES consolidation_runs(id)
124
- );
125
-
126
- CREATE INDEX IF NOT EXISTS idx_episodes_created ON episodes(created_at);
127
- CREATE INDEX IF NOT EXISTS idx_episodes_consolidated ON episodes(consolidated);
128
- CREATE INDEX IF NOT EXISTS idx_episodes_source ON episodes(source);
129
- CREATE INDEX IF NOT EXISTS idx_semantics_state ON semantics(state);
130
- CREATE INDEX IF NOT EXISTS idx_procedures_state ON procedures(state);
131
- CREATE INDEX IF NOT EXISTS idx_contradictions_state ON contradictions(state);
132
- CREATE INDEX IF NOT EXISTS idx_consolidation_status ON consolidation_runs(status);
133
- `;
134
-
135
- export function createVec0Tables(db, dimensions) {
136
- db.exec(`
137
- CREATE VIRTUAL TABLE IF NOT EXISTS vec_episodes USING vec0(
138
- id text primary key,
139
- embedding float[${dimensions}] distance_metric=cosine,
140
- source text,
141
- consolidated integer
142
- );
143
- `);
144
- db.exec(`
145
- CREATE VIRTUAL TABLE IF NOT EXISTS vec_semantics USING vec0(
146
- id text primary key,
147
- embedding float[${dimensions}] distance_metric=cosine,
148
- state text
149
- );
150
- `);
151
- db.exec(`
152
- CREATE VIRTUAL TABLE IF NOT EXISTS vec_procedures USING vec0(
153
- id text primary key,
154
- embedding float[${dimensions}] distance_metric=cosine,
155
- state text
156
- );
157
- `);
158
- }
159
-
160
- export function dropVec0Tables(db) {
161
- db.exec('DROP TABLE IF EXISTS vec_episodes');
162
- db.exec('DROP TABLE IF EXISTS vec_semantics');
163
- db.exec('DROP TABLE IF EXISTS vec_procedures');
164
- }
165
-
166
- function migrateTable(db, { source, target, selectCols, insertCols, placeholders, transform, dimensions }) {
167
- const count = db.prepare(`SELECT COUNT(*) as c FROM ${target}`).get().c;
168
- if (count > 0) return;
169
-
170
- const rows = db.prepare(`SELECT ${selectCols} FROM ${source} WHERE embedding IS NOT NULL`).all();
171
- if (rows.length === 0) return;
172
-
173
- const expectedBytes = dimensions ? dimensions * 4 : null;
174
- const insert = db.prepare(`INSERT INTO ${target}(${insertCols}) VALUES (${placeholders})`);
175
- const tx = db.transaction(() => {
176
- for (const row of rows) {
177
- if (expectedBytes && row.embedding.byteLength !== expectedBytes) continue;
178
- insert.run(...transform(row));
179
- }
180
- });
181
- tx();
182
- }
183
-
184
- function migrateEmbeddingsToVec0(db, dimensions) {
185
- migrateTable(db, {
186
- source: 'episodes',
187
- target: 'vec_episodes',
188
- selectCols: 'id, embedding, source, consolidated',
189
- insertCols: 'id, embedding, source, consolidated',
190
- placeholders: '?, ?, ?, ?',
191
- transform: (row) => [row.id, row.embedding, row.source, BigInt(row.consolidated ?? 0)],
192
- dimensions,
193
- });
194
-
195
- migrateTable(db, {
196
- source: 'semantics',
197
- target: 'vec_semantics',
198
- selectCols: 'id, embedding, state',
199
- insertCols: 'id, embedding, state',
200
- placeholders: '?, ?, ?',
201
- transform: (row) => [row.id, row.embedding, row.state],
202
- dimensions,
203
- });
204
-
205
- migrateTable(db, {
206
- source: 'procedures',
207
- target: 'vec_procedures',
208
- selectCols: 'id, embedding, state',
209
- insertCols: 'id, embedding, state',
210
- placeholders: '?, ?, ?',
211
- transform: (row) => [row.id, row.embedding, row.state],
212
- dimensions,
213
- });
214
- }
215
-
216
- function addColumnIfMissing(db, table, column, definition) {
217
- const columns = db.pragma(`table_info(${table})`);
218
- const exists = columns.some(col => col.name === column);
219
- if (!exists) {
220
- db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
221
- }
222
- }
223
-
224
- const SCHEMA_VERSION = 7;
225
-
226
- const MIGRATIONS = [
227
- { version: 1, up(db) { addColumnIfMissing(db, 'episodes', 'context', "TEXT DEFAULT '{}'"); } },
228
- { version: 2, up(db) { addColumnIfMissing(db, 'episodes', 'affect', "TEXT DEFAULT '{}'"); } },
229
- { version: 3, up(db) { addColumnIfMissing(db, 'semantics', 'interference_count', 'INTEGER DEFAULT 0'); } },
230
- { version: 4, up(db) { addColumnIfMissing(db, 'semantics', 'salience', 'REAL DEFAULT 0.5'); } },
231
- { version: 5, up(db) { addColumnIfMissing(db, 'procedures', 'interference_count', 'INTEGER DEFAULT 0'); } },
232
- { version: 6, up(db) { addColumnIfMissing(db, 'procedures', 'salience', 'REAL DEFAULT 0.5'); } },
233
- { version: 7, up(db) { addColumnIfMissing(db, 'episodes', 'private', 'INTEGER DEFAULT 0'); } },
234
- ];
235
-
236
- function runMigrations(db) {
237
- const row = db.prepare("SELECT value FROM audrey_config WHERE key = 'schema_version'").get();
238
- const currentVersion = row ? Number(row.value) : 0;
239
-
240
- if (currentVersion >= SCHEMA_VERSION) return;
241
-
242
- const pending = MIGRATIONS.filter(m => m.version > currentVersion);
243
- for (const migration of pending) {
244
- migration.up(db);
245
- }
246
-
247
- db.prepare(
248
- `INSERT INTO audrey_config (key, value) VALUES ('schema_version', ?)
249
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
250
- ).run(String(SCHEMA_VERSION));
251
- }
252
-
253
- /**
254
- * @param {string} dataDir
255
- * @param {{ dimensions?: number }} [options]
256
- * @returns {{ db: import('better-sqlite3').Database, migrated: boolean }}
257
- */
258
- export function createDatabase(dataDir, options = {}) {
259
- let { dimensions } = options;
260
- let migrated = false;
261
-
262
- mkdirSync(dataDir, { recursive: true });
263
- const dbPath = join(dataDir, 'audrey.db');
264
- const db = new Database(dbPath);
265
- db.pragma('journal_mode = WAL');
266
- db.pragma('foreign_keys = ON');
267
- db.pragma('busy_timeout = 5000');
268
- db.exec(SCHEMA);
269
- runMigrations(db);
270
-
271
- if (dimensions == null) {
272
- const stored = db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
273
- if (stored) {
274
- dimensions = parseInt(stored.value, 10);
275
- }
276
- }
277
-
278
- if (dimensions != null) {
279
- if (!Number.isInteger(dimensions) || dimensions <= 0) {
280
- throw new Error(`dimensions must be a positive integer, got: ${dimensions}`);
281
- }
282
-
283
- sqliteVec.load(db);
284
-
285
- const existing = db.prepare(
286
- "SELECT value FROM audrey_config WHERE key = 'dimensions'"
287
- ).get();
288
-
289
- if (existing) {
290
- const storedDims = parseInt(existing.value, 10);
291
- if (storedDims !== dimensions) {
292
- dropVec0Tables(db);
293
- db.prepare(
294
- "UPDATE audrey_config SET value = ? WHERE key = 'dimensions'"
295
- ).run(String(dimensions));
296
- migrated = true;
297
- }
298
- } else {
299
- db.prepare(
300
- "INSERT INTO audrey_config (key, value) VALUES ('dimensions', ?)"
301
- ).run(String(dimensions));
302
- }
303
-
304
- createVec0Tables(db, dimensions);
305
-
306
- if (!migrated) {
307
- migrateEmbeddingsToVec0(db, dimensions);
308
- }
309
- }
310
-
311
- return { db, migrated };
312
- }
313
-
314
- export function readStoredDimensions(dataDir) {
315
- const dbPath = join(dataDir, 'audrey.db');
316
- if (!existsSync(dbPath)) return null;
317
- const db = new Database(dbPath, { readonly: true });
318
- try {
319
- const row = db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
320
- return row ? parseInt(row.value, 10) : null;
321
- } catch (err) {
322
- if (err.message?.includes('no such table')) return null;
323
- throw err;
324
- } finally {
325
- db.close();
326
- }
327
- }
328
-
329
- export function closeDatabase(db) {
330
- if (db && db.open) {
331
- db.close();
332
- }
333
- }
1
+ import Database from 'better-sqlite3';
2
+ import * as sqliteVec from 'sqlite-vec';
3
+ import { join } from 'node:path';
4
+ import { mkdirSync, existsSync } from 'node:fs';
5
+
6
+ const SCHEMA = `
7
+ CREATE TABLE IF NOT EXISTS episodes (
8
+ id TEXT PRIMARY KEY,
9
+ content TEXT NOT NULL,
10
+ embedding BLOB,
11
+ source TEXT NOT NULL CHECK(source IN ('direct-observation','told-by-user','tool-result','inference','model-generated')),
12
+ source_reliability REAL NOT NULL,
13
+ salience REAL DEFAULT 0.5,
14
+ context TEXT DEFAULT '{}',
15
+ affect TEXT DEFAULT '{}',
16
+ tags TEXT,
17
+ causal_trigger TEXT,
18
+ causal_consequence TEXT,
19
+ created_at TEXT NOT NULL,
20
+ embedding_model TEXT,
21
+ embedding_version TEXT,
22
+ supersedes TEXT,
23
+ superseded_by TEXT,
24
+ consolidated INTEGER DEFAULT 0,
25
+ private INTEGER DEFAULT 0,
26
+ FOREIGN KEY (supersedes) REFERENCES episodes(id)
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS semantics (
30
+ id TEXT PRIMARY KEY,
31
+ content TEXT NOT NULL,
32
+ embedding BLOB,
33
+ state TEXT DEFAULT 'active' CHECK(state IN ('active','disputed','superseded','context_dependent','dormant','rolled_back')),
34
+ conditions TEXT,
35
+ evidence_episode_ids TEXT,
36
+ evidence_count INTEGER DEFAULT 0,
37
+ supporting_count INTEGER DEFAULT 0,
38
+ contradicting_count INTEGER DEFAULT 0,
39
+ source_type_diversity INTEGER DEFAULT 0,
40
+ consolidation_checkpoint TEXT,
41
+ embedding_model TEXT,
42
+ embedding_version TEXT,
43
+ consolidation_model TEXT,
44
+ consolidation_prompt_hash TEXT,
45
+ created_at TEXT NOT NULL,
46
+ last_reinforced_at TEXT,
47
+ retrieval_count INTEGER DEFAULT 0,
48
+ challenge_count INTEGER DEFAULT 0,
49
+ interference_count INTEGER DEFAULT 0,
50
+ salience REAL DEFAULT 0.5
51
+ );
52
+
53
+ CREATE TABLE IF NOT EXISTS procedures (
54
+ id TEXT PRIMARY KEY,
55
+ content TEXT NOT NULL,
56
+ embedding BLOB,
57
+ state TEXT DEFAULT 'active' CHECK(state IN ('active','disputed','superseded','context_dependent','dormant','rolled_back')),
58
+ trigger_conditions TEXT,
59
+ evidence_episode_ids TEXT,
60
+ success_count INTEGER DEFAULT 0,
61
+ failure_count INTEGER DEFAULT 0,
62
+ embedding_model TEXT,
63
+ embedding_version TEXT,
64
+ created_at TEXT NOT NULL,
65
+ last_reinforced_at TEXT,
66
+ retrieval_count INTEGER DEFAULT 0,
67
+ interference_count INTEGER DEFAULT 0,
68
+ salience REAL DEFAULT 0.5
69
+ );
70
+
71
+ CREATE TABLE IF NOT EXISTS causal_links (
72
+ id TEXT PRIMARY KEY,
73
+ cause_id TEXT NOT NULL,
74
+ effect_id TEXT NOT NULL,
75
+ link_type TEXT DEFAULT 'causal' CHECK(link_type IN ('causal','correlational','temporal')),
76
+ mechanism TEXT,
77
+ confidence REAL,
78
+ evidence_count INTEGER DEFAULT 1,
79
+ created_at TEXT NOT NULL
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS contradictions (
83
+ id TEXT PRIMARY KEY,
84
+ claim_a_id TEXT NOT NULL,
85
+ claim_b_id TEXT NOT NULL,
86
+ claim_a_type TEXT NOT NULL,
87
+ claim_b_type TEXT NOT NULL,
88
+ state TEXT DEFAULT 'open' CHECK(state IN ('open','resolved','context_dependent','reopened')),
89
+ resolution TEXT,
90
+ resolved_at TEXT,
91
+ reopened_at TEXT,
92
+ reopen_evidence_id TEXT,
93
+ created_at TEXT NOT NULL
94
+ );
95
+
96
+ CREATE TABLE IF NOT EXISTS consolidation_runs (
97
+ id TEXT PRIMARY KEY,
98
+ checkpoint_cursor TEXT,
99
+ input_episode_ids TEXT,
100
+ output_memory_ids TEXT,
101
+ confidence_deltas TEXT,
102
+ consolidation_model TEXT,
103
+ consolidation_prompt_hash TEXT,
104
+ started_at TEXT,
105
+ completed_at TEXT,
106
+ status TEXT DEFAULT 'running' CHECK(status IN ('running','completed','failed','rolled_back'))
107
+ );
108
+
109
+ CREATE TABLE IF NOT EXISTS audrey_config (
110
+ key TEXT PRIMARY KEY,
111
+ value TEXT NOT NULL
112
+ );
113
+
114
+ CREATE TABLE IF NOT EXISTS consolidation_metrics (
115
+ id TEXT PRIMARY KEY,
116
+ run_id TEXT NOT NULL,
117
+ min_cluster_size INTEGER NOT NULL,
118
+ similarity_threshold REAL NOT NULL,
119
+ episodes_evaluated INTEGER NOT NULL,
120
+ clusters_found INTEGER NOT NULL,
121
+ principles_extracted INTEGER NOT NULL,
122
+ created_at TEXT NOT NULL,
123
+ FOREIGN KEY (run_id) REFERENCES consolidation_runs(id)
124
+ );
125
+
126
+ CREATE INDEX IF NOT EXISTS idx_episodes_created ON episodes(created_at);
127
+ CREATE INDEX IF NOT EXISTS idx_episodes_consolidated ON episodes(consolidated);
128
+ CREATE INDEX IF NOT EXISTS idx_episodes_source ON episodes(source);
129
+ CREATE INDEX IF NOT EXISTS idx_semantics_state ON semantics(state);
130
+ CREATE INDEX IF NOT EXISTS idx_procedures_state ON procedures(state);
131
+ CREATE INDEX IF NOT EXISTS idx_contradictions_state ON contradictions(state);
132
+ CREATE INDEX IF NOT EXISTS idx_consolidation_status ON consolidation_runs(status);
133
+ `;
134
+
135
+ export function createVec0Tables(db, dimensions) {
136
+ db.exec(`
137
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_episodes USING vec0(
138
+ id text primary key,
139
+ embedding float[${dimensions}] distance_metric=cosine,
140
+ source text,
141
+ consolidated integer
142
+ );
143
+ `);
144
+ db.exec(`
145
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_semantics USING vec0(
146
+ id text primary key,
147
+ embedding float[${dimensions}] distance_metric=cosine,
148
+ state text
149
+ );
150
+ `);
151
+ db.exec(`
152
+ CREATE VIRTUAL TABLE IF NOT EXISTS vec_procedures USING vec0(
153
+ id text primary key,
154
+ embedding float[${dimensions}] distance_metric=cosine,
155
+ state text
156
+ );
157
+ `);
158
+ }
159
+
160
+ export function dropVec0Tables(db) {
161
+ db.exec('DROP TABLE IF EXISTS vec_episodes');
162
+ db.exec('DROP TABLE IF EXISTS vec_semantics');
163
+ db.exec('DROP TABLE IF EXISTS vec_procedures');
164
+ }
165
+
166
+ function migrateTable(db, { source, target, selectCols, insertCols, placeholders, transform, dimensions }) {
167
+ const count = db.prepare(`SELECT COUNT(*) as c FROM ${target}`).get().c;
168
+ if (count > 0) return;
169
+
170
+ const rows = db.prepare(`SELECT ${selectCols} FROM ${source} WHERE embedding IS NOT NULL`).all();
171
+ if (rows.length === 0) return;
172
+
173
+ const expectedBytes = dimensions ? dimensions * 4 : null;
174
+ const insert = db.prepare(`INSERT INTO ${target}(${insertCols}) VALUES (${placeholders})`);
175
+ const tx = db.transaction(() => {
176
+ for (const row of rows) {
177
+ if (expectedBytes && row.embedding.byteLength !== expectedBytes) continue;
178
+ insert.run(...transform(row));
179
+ }
180
+ });
181
+ tx();
182
+ }
183
+
184
+ function migrateEmbeddingsToVec0(db, dimensions) {
185
+ migrateTable(db, {
186
+ source: 'episodes',
187
+ target: 'vec_episodes',
188
+ selectCols: 'id, embedding, source, consolidated',
189
+ insertCols: 'id, embedding, source, consolidated',
190
+ placeholders: '?, ?, ?, ?',
191
+ transform: (row) => [row.id, row.embedding, row.source, BigInt(row.consolidated ?? 0)],
192
+ dimensions,
193
+ });
194
+
195
+ migrateTable(db, {
196
+ source: 'semantics',
197
+ target: 'vec_semantics',
198
+ selectCols: 'id, embedding, state',
199
+ insertCols: 'id, embedding, state',
200
+ placeholders: '?, ?, ?',
201
+ transform: (row) => [row.id, row.embedding, row.state],
202
+ dimensions,
203
+ });
204
+
205
+ migrateTable(db, {
206
+ source: 'procedures',
207
+ target: 'vec_procedures',
208
+ selectCols: 'id, embedding, state',
209
+ insertCols: 'id, embedding, state',
210
+ placeholders: '?, ?, ?',
211
+ transform: (row) => [row.id, row.embedding, row.state],
212
+ dimensions,
213
+ });
214
+ }
215
+
216
+ function addColumnIfMissing(db, table, column, definition) {
217
+ const columns = db.pragma(`table_info(${table})`);
218
+ const exists = columns.some(col => col.name === column);
219
+ if (!exists) {
220
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
221
+ }
222
+ }
223
+
224
+ const SCHEMA_VERSION = 7;
225
+
226
+ const MIGRATIONS = [
227
+ { version: 1, up(db) { addColumnIfMissing(db, 'episodes', 'context', "TEXT DEFAULT '{}'"); } },
228
+ { version: 2, up(db) { addColumnIfMissing(db, 'episodes', 'affect', "TEXT DEFAULT '{}'"); } },
229
+ { version: 3, up(db) { addColumnIfMissing(db, 'semantics', 'interference_count', 'INTEGER DEFAULT 0'); } },
230
+ { version: 4, up(db) { addColumnIfMissing(db, 'semantics', 'salience', 'REAL DEFAULT 0.5'); } },
231
+ { version: 5, up(db) { addColumnIfMissing(db, 'procedures', 'interference_count', 'INTEGER DEFAULT 0'); } },
232
+ { version: 6, up(db) { addColumnIfMissing(db, 'procedures', 'salience', 'REAL DEFAULT 0.5'); } },
233
+ { version: 7, up(db) { addColumnIfMissing(db, 'episodes', 'private', 'INTEGER DEFAULT 0'); } },
234
+ ];
235
+
236
+ function runMigrations(db) {
237
+ const row = db.prepare("SELECT value FROM audrey_config WHERE key = 'schema_version'").get();
238
+ const currentVersion = row ? Number(row.value) : 0;
239
+
240
+ if (currentVersion >= SCHEMA_VERSION) return;
241
+
242
+ const pending = MIGRATIONS.filter(m => m.version > currentVersion);
243
+ for (const migration of pending) {
244
+ migration.up(db);
245
+ }
246
+
247
+ db.prepare(
248
+ `INSERT INTO audrey_config (key, value) VALUES ('schema_version', ?)
249
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`
250
+ ).run(String(SCHEMA_VERSION));
251
+ }
252
+
253
+ /**
254
+ * @param {string} dataDir
255
+ * @param {{ dimensions?: number }} [options]
256
+ * @returns {{ db: import('better-sqlite3').Database, migrated: boolean }}
257
+ */
258
+ export function createDatabase(dataDir, options = {}) {
259
+ let { dimensions } = options;
260
+ let migrated = false;
261
+
262
+ mkdirSync(dataDir, { recursive: true });
263
+ const dbPath = join(dataDir, 'audrey.db');
264
+ const db = new Database(dbPath);
265
+ db.pragma('journal_mode = WAL');
266
+ db.pragma('foreign_keys = ON');
267
+ db.pragma('busy_timeout = 5000');
268
+ db.exec(SCHEMA);
269
+ runMigrations(db);
270
+
271
+ if (dimensions == null) {
272
+ const stored = db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
273
+ if (stored) {
274
+ dimensions = parseInt(stored.value, 10);
275
+ }
276
+ }
277
+
278
+ if (dimensions != null) {
279
+ if (!Number.isInteger(dimensions) || dimensions <= 0) {
280
+ throw new Error(`dimensions must be a positive integer, got: ${dimensions}`);
281
+ }
282
+
283
+ sqliteVec.load(db);
284
+
285
+ const existing = db.prepare(
286
+ "SELECT value FROM audrey_config WHERE key = 'dimensions'"
287
+ ).get();
288
+
289
+ if (existing) {
290
+ const storedDims = parseInt(existing.value, 10);
291
+ if (storedDims !== dimensions) {
292
+ dropVec0Tables(db);
293
+ db.prepare(
294
+ "UPDATE audrey_config SET value = ? WHERE key = 'dimensions'"
295
+ ).run(String(dimensions));
296
+ migrated = true;
297
+ }
298
+ } else {
299
+ db.prepare(
300
+ "INSERT INTO audrey_config (key, value) VALUES ('dimensions', ?)"
301
+ ).run(String(dimensions));
302
+ }
303
+
304
+ createVec0Tables(db, dimensions);
305
+
306
+ if (!migrated) {
307
+ migrateEmbeddingsToVec0(db, dimensions);
308
+ }
309
+ }
310
+
311
+ return { db, migrated };
312
+ }
313
+
314
+ export function readStoredDimensions(dataDir) {
315
+ const dbPath = join(dataDir, 'audrey.db');
316
+ if (!existsSync(dbPath)) return null;
317
+ const db = new Database(dbPath, { readonly: true });
318
+ try {
319
+ const row = db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
320
+ return row ? parseInt(row.value, 10) : null;
321
+ } catch (err) {
322
+ if (err.message?.includes('no such table')) return null;
323
+ throw err;
324
+ } finally {
325
+ db.close();
326
+ }
327
+ }
328
+
329
+ export function closeDatabase(db) {
330
+ if (db && db.open) {
331
+ db.close();
332
+ }
333
+ }
package/src/encode.js CHANGED
@@ -1,63 +1,85 @@
1
- import { generateId } from './ulid.js';
2
- import { sourceReliability } from './confidence.js';
3
- import { arousalSalienceBoost } from './affect.js';
4
-
5
- /**
6
- * @param {import('better-sqlite3').Database} db
7
- * @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
8
- * @param {{ content: string, source: string, salience?: number, causal?: { trigger?: string, consequence?: string }, tags?: string[], supersedes?: string, context?: object, affect?: object, arousalWeight?: number, private?: boolean }} params
9
- * @returns {Promise<string>}
10
- */
11
- export async function encodeEpisode(db, embeddingProvider, {
12
- content,
13
- source,
14
- salience = 0.5,
15
- causal,
16
- tags,
17
- supersedes,
18
- context = {},
19
- affect = {},
20
- arousalWeight = 0.3,
21
- private: isPrivate = false,
22
- }) {
23
- if (!content || typeof content !== 'string') throw new Error('content must be a non-empty string');
24
- if (salience < 0 || salience > 1) throw new Error('salience must be between 0 and 1');
25
- if (tags && !Array.isArray(tags)) throw new Error('tags must be an array');
26
-
27
- const reliability = sourceReliability(source);
28
- const vector = await embeddingProvider.embed(content);
29
- const embeddingBuffer = embeddingProvider.vectorToBuffer(vector);
30
- const id = generateId();
31
- const now = new Date().toISOString();
32
-
33
- const boost = arousalSalienceBoost(affect.arousal);
34
- const effectiveSalience = Math.min(1.0, salience + (boost * arousalWeight));
35
-
36
- const insertAndLink = db.transaction(() => {
37
- db.prepare(`
38
- INSERT INTO episodes (
39
- id, content, embedding, source, source_reliability, salience, context, affect,
40
- tags, causal_trigger, causal_consequence, created_at,
41
- embedding_model, embedding_version, supersedes, "private"
42
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
43
- `).run(
44
- id, content, embeddingBuffer, source, reliability, effectiveSalience,
45
- JSON.stringify(context),
46
- JSON.stringify(affect),
47
- tags ? JSON.stringify(tags) : null,
48
- causal?.trigger || null, causal?.consequence || null,
49
- now, embeddingProvider.modelName, embeddingProvider.modelVersion,
50
- supersedes || null,
51
- isPrivate ? 1 : 0,
52
- );
53
- db.prepare(
54
- 'INSERT INTO vec_episodes(id, embedding, source, consolidated) VALUES (?, ?, ?, ?)'
55
- ).run(id, embeddingBuffer, source, BigInt(0));
56
- if (supersedes) {
57
- db.prepare('UPDATE episodes SET superseded_by = ? WHERE id = ?').run(id, supersedes);
58
- }
59
- });
60
-
61
- insertAndLink();
62
- return id;
63
- }
1
+ import { generateId } from './ulid.js';
2
+ import { sourceReliability } from './confidence.js';
3
+ import { arousalSalienceBoost } from './affect.js';
4
+
5
+ /**
6
+ * @param {import('better-sqlite3').Database} db
7
+ * @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
8
+ * @param {{ content: string, source: string, salience?: number, causal?: { trigger?: string, consequence?: string }, tags?: string[], supersedes?: string, context?: object, affect?: object, arousalWeight?: number, private?: boolean }} params
9
+ * @returns {Promise<string>}
10
+ */
11
+ export async function encodeEpisode(db, embeddingProvider, {
12
+ content,
13
+ source,
14
+ salience = 0.5,
15
+ causal,
16
+ tags,
17
+ supersedes,
18
+ autoSupersede = false,
19
+ autoSupersedeThreshold = 0.95,
20
+ context = {},
21
+ affect = {},
22
+ arousalWeight = 0.3,
23
+ private: isPrivate = false,
24
+ }) {
25
+ if (!content || typeof content !== 'string') throw new Error('content must be a non-empty string');
26
+ if (salience < 0 || salience > 1) throw new Error('salience must be between 0 and 1');
27
+ if (tags && !Array.isArray(tags)) throw new Error('tags must be an array');
28
+
29
+ const reliability = sourceReliability(source);
30
+ const vector = await embeddingProvider.embed(content);
31
+ const embeddingBuffer = embeddingProvider.vectorToBuffer(vector);
32
+
33
+ // Auto-supersede: find the most similar existing episode and supersede it
34
+ if (autoSupersede && !supersedes) {
35
+ try {
36
+ const match = db.prepare(`
37
+ SELECT v.id, (1.0 - v.distance) AS similarity
38
+ FROM vec_episodes v
39
+ JOIN episodes e ON e.id = v.id
40
+ WHERE v.embedding MATCH ?
41
+ AND k = 1
42
+ AND e.superseded_by IS NULL
43
+ `).get(embeddingBuffer);
44
+
45
+ if (match && match.similarity >= autoSupersedeThreshold) {
46
+ supersedes = match.id;
47
+ }
48
+ } catch {
49
+ // vec table might be empty — proceed without supersession
50
+ }
51
+ }
52
+ const id = generateId();
53
+ const now = new Date().toISOString();
54
+
55
+ const boost = arousalSalienceBoost(affect.arousal);
56
+ const effectiveSalience = Math.min(1.0, salience + (boost * arousalWeight));
57
+
58
+ const insertAndLink = db.transaction(() => {
59
+ db.prepare(`
60
+ INSERT INTO episodes (
61
+ id, content, embedding, source, source_reliability, salience, context, affect,
62
+ tags, causal_trigger, causal_consequence, created_at,
63
+ embedding_model, embedding_version, supersedes, "private"
64
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
65
+ `).run(
66
+ id, content, embeddingBuffer, source, reliability, effectiveSalience,
67
+ JSON.stringify(context),
68
+ JSON.stringify(affect),
69
+ tags ? JSON.stringify(tags) : null,
70
+ causal?.trigger || null, causal?.consequence || null,
71
+ now, embeddingProvider.modelName, embeddingProvider.modelVersion,
72
+ supersedes || null,
73
+ isPrivate ? 1 : 0,
74
+ );
75
+ db.prepare(
76
+ 'INSERT INTO vec_episodes(id, embedding, source, consolidated) VALUES (?, ?, ?, ?)'
77
+ ).run(id, embeddingBuffer, source, BigInt(0));
78
+ if (supersedes) {
79
+ db.prepare('UPDATE episodes SET superseded_by = ? WHERE id = ?').run(id, supersedes);
80
+ }
81
+ });
82
+
83
+ insertAndLink();
84
+ return id;
85
+ }