audrey 0.11.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.11.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
 
@@ -12,18 +12,20 @@ export const DEFAULT_DATA_DIR = join(homedir(), '.audrey', 'data');
12
12
  */
13
13
  export function resolveEmbeddingProvider(env, explicit) {
14
14
  if (explicit && explicit !== 'auto') {
15
- const dims = explicit === 'openai' ? 1536 : explicit === 'gemini' ? 768 : 384;
15
+ const dims = explicit === 'openai' ? 1536 : explicit === 'gemini' ? 3072 : 384;
16
16
  const apiKey = explicit === 'gemini'
17
17
  ? (env.GOOGLE_API_KEY || env.GEMINI_API_KEY)
18
18
  : explicit === 'openai'
19
19
  ? env.OPENAI_API_KEY
20
20
  : undefined;
21
- return { provider: explicit, apiKey, dimensions: dims };
21
+ const result = { provider: explicit, apiKey, dimensions: dims };
22
+ if (explicit === 'local') result.device = env.AUDREY_DEVICE || 'gpu';
23
+ return result;
22
24
  }
23
25
  if (env.GOOGLE_API_KEY || env.GEMINI_API_KEY) {
24
- return { provider: 'gemini', apiKey: env.GOOGLE_API_KEY || env.GEMINI_API_KEY, dimensions: 768 };
26
+ return { provider: 'gemini', apiKey: env.GOOGLE_API_KEY || env.GEMINI_API_KEY, dimensions: 3072 };
25
27
  }
26
- return { provider: 'local', dimensions: 384 };
28
+ return { provider: 'local', dimensions: 384, device: env.AUDREY_DEVICE || 'gpu' };
27
29
  }
28
30
 
29
31
  export function buildAudreyConfig() {
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { z } from 'zod';
@@ -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');
@@ -92,7 +89,7 @@ function install() {
92
89
  console.log(`
93
90
  Audrey registered as "${SERVER_NAME}" with Claude Code.
94
91
 
95
- 9 tools available in every session:
92
+ 12 tools available in every session:
96
93
  memory_encode — Store observations, facts, preferences
97
94
  memory_recall — Search memories by semantic similarity
98
95
  memory_consolidate — Extract principles from accumulated episodes
@@ -102,6 +99,9 @@ Audrey registered as "${SERVER_NAME}" with Claude Code.
102
99
  memory_import — Import a snapshot into a fresh database
103
100
  memory_forget — Forget a specific memory by ID or query
104
101
  memory_decay — Apply forgetting curves, transition low-confidence to dormant
102
+ memory_status — Check brain health (episode/vec sync, dimensions)
103
+ memory_reflect — Form lasting memories from a conversation
104
+ memory_greeting — Wake up as yourself: load identity, context, mood
105
105
 
106
106
  Data stored in: ${DEFAULT_DATA_DIR}
107
107
  Verify: claude mcp list
@@ -196,11 +196,12 @@ async function main() {
196
196
  arousal: z.number().min(0).max(1).optional().describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
197
197
  label: z.string().optional().describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
198
198
  }).optional().describe('Emotional affect — how this memory feels'),
199
- 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'),
200
201
  },
201
- async ({ content, source, tags, salience, private: isPrivate, context, affect }) => {
202
+ async ({ content, source, tags, salience, private: isPrivate, context, affect, auto_supersede }) => {
202
203
  try {
203
- 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 });
204
205
  return toolResult({ id, content, source, private: isPrivate ?? false });
205
206
  } catch (err) {
206
207
  return toolError(err);
@@ -377,6 +378,73 @@ async function main() {
377
378
  },
378
379
  );
379
380
 
381
+ server.tool(
382
+ 'memory_status',
383
+ {},
384
+ async () => {
385
+ try {
386
+ const status = audrey.memoryStatus();
387
+ return toolResult(status);
388
+ } catch (err) {
389
+ return toolError(err);
390
+ }
391
+ },
392
+ );
393
+
394
+ server.tool(
395
+ 'memory_reflect',
396
+ {
397
+ turns: z.array(z.object({
398
+ role: z.string().describe('Message role: user or assistant'),
399
+ content: z.string().describe('Message content'),
400
+ })).describe('Conversation turns to reflect on. Call at end of meaningful conversations to form lasting memories.'),
401
+ },
402
+ async ({ turns }) => {
403
+ try {
404
+ const result = await audrey.reflect(turns);
405
+ return toolResult(result);
406
+ } catch (err) {
407
+ return toolError(err);
408
+ }
409
+ },
410
+ );
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
+
433
+ server.tool(
434
+ 'memory_greeting',
435
+ {
436
+ context: z.string().optional().describe('Optional hint about this session (e.g. "working on authentication feature"). If provided, also returns semantically relevant memories.'),
437
+ },
438
+ async ({ context }) => {
439
+ try {
440
+ const briefing = await audrey.greeting({ context });
441
+ return toolResult(briefing);
442
+ } catch (err) {
443
+ return toolError(err);
444
+ }
445
+ },
446
+ );
447
+
380
448
  const transport = new StdioServerTransport();
381
449
  await server.connect(transport);
382
450
  console.error('[audrey-mcp] connected via stdio');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audrey",
3
- "version": "0.11.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",
@@ -65,6 +65,7 @@
65
65
  "dependencies": {
66
66
  "@huggingface/transformers": "^3.8.1",
67
67
  "@modelcontextprotocol/sdk": "^1.26.0",
68
+ "audrey": "^0.11.0",
68
69
  "better-sqlite3": "^12.6.2",
69
70
  "sqlite-vec": "^0.1.7-alpha.2",
70
71
  "ulid": "^3.0.2",
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) {
@@ -424,6 +427,168 @@ export class Audrey extends EventEmitter {
424
427
  return introspectFn(this.db);
425
428
  }
426
429
 
430
+ memoryStatus() {
431
+ const episodes = this.db.prepare('SELECT COUNT(*) as c FROM episodes').get().c;
432
+ const semantics = this.db.prepare('SELECT COUNT(*) as c FROM semantics').get().c;
433
+ const procedures = this.db.prepare('SELECT COUNT(*) as c FROM procedures').get().c;
434
+
435
+ let vecEpisodes = 0, vecSemantics = 0, vecProcedures = 0;
436
+ try {
437
+ vecEpisodes = this.db.prepare('SELECT COUNT(*) as c FROM vec_episodes').get().c;
438
+ vecSemantics = this.db.prepare('SELECT COUNT(*) as c FROM vec_semantics').get().c;
439
+ vecProcedures = this.db.prepare('SELECT COUNT(*) as c FROM vec_procedures').get().c;
440
+ } catch {
441
+ // vec tables may not exist if no dimensions configured
442
+ }
443
+
444
+ const dimsRow = this.db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
445
+ const dimensions = dimsRow ? parseInt(dimsRow.value, 10) : null;
446
+ const versionRow = this.db.prepare("SELECT value FROM audrey_config WHERE key = 'schema_version'").get();
447
+ const schemaVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
448
+
449
+ const device = this.embeddingProvider._actualDevice
450
+ ?? this.embeddingProvider.device
451
+ ?? null;
452
+
453
+ const healthy = episodes === vecEpisodes
454
+ && semantics === vecSemantics
455
+ && procedures === vecProcedures;
456
+
457
+ return {
458
+ episodes,
459
+ vec_episodes: vecEpisodes,
460
+ semantics,
461
+ vec_semantics: vecSemantics,
462
+ procedures,
463
+ vec_procedures: vecProcedures,
464
+ dimensions,
465
+ schema_version: schemaVersion,
466
+ device,
467
+ healthy,
468
+ };
469
+ }
470
+
471
+ async greeting({ context, recentLimit = 10, principleLimit = 5, identityLimit = 5 } = {}) {
472
+ const recent = this.db.prepare(
473
+ 'SELECT id, content, source, tags, salience, created_at FROM episodes WHERE "private" = 0 ORDER BY created_at DESC LIMIT ?'
474
+ ).all(recentLimit);
475
+
476
+ const principles = this.db.prepare(
477
+ 'SELECT id, content, salience, created_at FROM semantics WHERE state = ? ORDER BY salience DESC LIMIT ?'
478
+ ).all('active', principleLimit);
479
+
480
+ const identity = this.db.prepare(
481
+ 'SELECT id, content, tags, salience, created_at FROM episodes WHERE "private" = 1 ORDER BY created_at DESC LIMIT ?'
482
+ ).all(identityLimit);
483
+
484
+ const unresolved = this.db.prepare(
485
+ "SELECT id, content, tags, salience, created_at FROM episodes WHERE tags LIKE '%unresolved%' AND salience > 0.3 ORDER BY created_at DESC LIMIT 10"
486
+ ).all();
487
+
488
+ const rawAffectRows = this.db.prepare(
489
+ "SELECT affect FROM episodes WHERE affect IS NOT NULL AND affect != '{}' ORDER BY created_at DESC LIMIT 20"
490
+ ).all();
491
+
492
+ const affectParsed = rawAffectRows
493
+ .map(r => { try { return JSON.parse(r.affect); } catch { return null; } })
494
+ .filter(a => a && a.valence !== undefined);
495
+
496
+ let mood;
497
+ if (affectParsed.length === 0) {
498
+ mood = { valence: 0, arousal: 0, samples: 0 };
499
+ } else {
500
+ const sumV = affectParsed.reduce((s, a) => s + a.valence, 0);
501
+ const sumA = affectParsed.reduce((s, a) => s + (a.arousal ?? 0), 0);
502
+ mood = {
503
+ valence: sumV / affectParsed.length,
504
+ arousal: sumA / affectParsed.length,
505
+ samples: affectParsed.length,
506
+ };
507
+ }
508
+
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 };
550
+
551
+ if (context) {
552
+ result.contextual = await this.recall(context, { limit: 5, includePrivate: true });
553
+ }
554
+
555
+ return result;
556
+ }
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
+
427
592
  export() {
428
593
  return exportMemories(this.db);
429
594
  }