audrey 0.11.0 → 0.14.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.14.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';
@@ -92,7 +92,7 @@ function install() {
92
92
  console.log(`
93
93
  Audrey registered as "${SERVER_NAME}" with Claude Code.
94
94
 
95
- 9 tools available in every session:
95
+ 12 tools available in every session:
96
96
  memory_encode — Store observations, facts, preferences
97
97
  memory_recall — Search memories by semantic similarity
98
98
  memory_consolidate — Extract principles from accumulated episodes
@@ -102,6 +102,9 @@ Audrey registered as "${SERVER_NAME}" with Claude Code.
102
102
  memory_import — Import a snapshot into a fresh database
103
103
  memory_forget — Forget a specific memory by ID or query
104
104
  memory_decay — Apply forgetting curves, transition low-confidence to dormant
105
+ memory_status — Check brain health (episode/vec sync, dimensions)
106
+ memory_reflect — Form lasting memories from a conversation
107
+ memory_greeting — Wake up as yourself: load identity, context, mood
105
108
 
106
109
  Data stored in: ${DEFAULT_DATA_DIR}
107
110
  Verify: claude mcp list
@@ -196,7 +199,7 @@ async function main() {
196
199
  arousal: z.number().min(0).max(1).optional().describe('Emotional arousal: 0 (calm) to 1 (highly activated)'),
197
200
  label: z.string().optional().describe('Human-readable emotion label (e.g., "curiosity", "frustration", "relief")'),
198
201
  }).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'),
202
+ private: z.boolean().optional().describe('If true, memory is only visible to the AI � excluded from public recall results'),
200
203
  },
201
204
  async ({ content, source, tags, salience, private: isPrivate, context, affect }) => {
202
205
  try {
@@ -377,6 +380,52 @@ async function main() {
377
380
  },
378
381
  );
379
382
 
383
+ server.tool(
384
+ 'memory_status',
385
+ {},
386
+ async () => {
387
+ try {
388
+ const status = audrey.memoryStatus();
389
+ return toolResult(status);
390
+ } catch (err) {
391
+ return toolError(err);
392
+ }
393
+ },
394
+ );
395
+
396
+ server.tool(
397
+ 'memory_reflect',
398
+ {
399
+ turns: z.array(z.object({
400
+ role: z.string().describe('Message role: user or assistant'),
401
+ content: z.string().describe('Message content'),
402
+ })).describe('Conversation turns to reflect on. Call at end of meaningful conversations to form lasting memories.'),
403
+ },
404
+ async ({ turns }) => {
405
+ try {
406
+ const result = await audrey.reflect(turns);
407
+ return toolResult(result);
408
+ } catch (err) {
409
+ return toolError(err);
410
+ }
411
+ },
412
+ );
413
+
414
+ server.tool(
415
+ 'memory_greeting',
416
+ {
417
+ context: z.string().optional().describe('Optional hint about this session (e.g. "working on authentication feature"). If provided, also returns semantically relevant memories.'),
418
+ },
419
+ async ({ context }) => {
420
+ try {
421
+ const briefing = await audrey.greeting({ context });
422
+ return toolResult(briefing);
423
+ } catch (err) {
424
+ return toolError(err);
425
+ }
426
+ },
427
+ );
428
+
380
429
  const transport = new StdioServerTransport();
381
430
  await server.connect(transport);
382
431
  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.14.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
@@ -424,6 +424,94 @@ export class Audrey extends EventEmitter {
424
424
  return introspectFn(this.db);
425
425
  }
426
426
 
427
+ memoryStatus() {
428
+ const episodes = this.db.prepare('SELECT COUNT(*) as c FROM episodes').get().c;
429
+ const semantics = this.db.prepare('SELECT COUNT(*) as c FROM semantics').get().c;
430
+ const procedures = this.db.prepare('SELECT COUNT(*) as c FROM procedures').get().c;
431
+
432
+ let vecEpisodes = 0, vecSemantics = 0, vecProcedures = 0;
433
+ try {
434
+ vecEpisodes = this.db.prepare('SELECT COUNT(*) as c FROM vec_episodes').get().c;
435
+ vecSemantics = this.db.prepare('SELECT COUNT(*) as c FROM vec_semantics').get().c;
436
+ vecProcedures = this.db.prepare('SELECT COUNT(*) as c FROM vec_procedures').get().c;
437
+ } catch {
438
+ // vec tables may not exist if no dimensions configured
439
+ }
440
+
441
+ const dimsRow = this.db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
442
+ const dimensions = dimsRow ? parseInt(dimsRow.value, 10) : null;
443
+ const versionRow = this.db.prepare("SELECT value FROM audrey_config WHERE key = 'schema_version'").get();
444
+ const schemaVersion = versionRow ? parseInt(versionRow.value, 10) : 0;
445
+
446
+ const device = this.embeddingProvider._actualDevice
447
+ ?? this.embeddingProvider.device
448
+ ?? null;
449
+
450
+ const healthy = episodes === vecEpisodes
451
+ && semantics === vecSemantics
452
+ && procedures === vecProcedures;
453
+
454
+ return {
455
+ episodes,
456
+ vec_episodes: vecEpisodes,
457
+ semantics,
458
+ vec_semantics: vecSemantics,
459
+ procedures,
460
+ vec_procedures: vecProcedures,
461
+ dimensions,
462
+ schema_version: schemaVersion,
463
+ device,
464
+ healthy,
465
+ };
466
+ }
467
+
468
+ async greeting({ context, recentLimit = 10, principleLimit = 5, identityLimit = 5 } = {}) {
469
+ const recent = this.db.prepare(
470
+ 'SELECT id, content, source, tags, salience, created_at FROM episodes WHERE "private" = 0 ORDER BY created_at DESC LIMIT ?'
471
+ ).all(recentLimit);
472
+
473
+ const principles = this.db.prepare(
474
+ 'SELECT id, content, salience, created_at FROM semantics WHERE state = ? ORDER BY salience DESC LIMIT ?'
475
+ ).all('active', principleLimit);
476
+
477
+ const identity = this.db.prepare(
478
+ 'SELECT id, content, tags, salience, created_at FROM episodes WHERE "private" = 1 ORDER BY created_at DESC LIMIT ?'
479
+ ).all(identityLimit);
480
+
481
+ const unresolved = this.db.prepare(
482
+ "SELECT id, content, tags, salience, created_at FROM episodes WHERE tags LIKE '%unresolved%' AND salience > 0.3 ORDER BY created_at DESC LIMIT 10"
483
+ ).all();
484
+
485
+ const rawAffectRows = this.db.prepare(
486
+ "SELECT affect FROM episodes WHERE affect IS NOT NULL AND affect != '{}' ORDER BY created_at DESC LIMIT 20"
487
+ ).all();
488
+
489
+ const affectParsed = rawAffectRows
490
+ .map(r => { try { return JSON.parse(r.affect); } catch { return null; } })
491
+ .filter(a => a && a.valence !== undefined);
492
+
493
+ let mood;
494
+ if (affectParsed.length === 0) {
495
+ mood = { valence: 0, arousal: 0, samples: 0 };
496
+ } else {
497
+ const sumV = affectParsed.reduce((s, a) => s + a.valence, 0);
498
+ const sumA = affectParsed.reduce((s, a) => s + (a.arousal ?? 0), 0);
499
+ mood = {
500
+ valence: sumV / affectParsed.length,
501
+ arousal: sumA / affectParsed.length,
502
+ samples: affectParsed.length,
503
+ };
504
+ }
505
+
506
+ const result = { recent, principles, mood, unresolved, identity };
507
+
508
+ if (context) {
509
+ result.contextual = await this.recall(context, { limit: 5, includePrivate: true });
510
+ }
511
+
512
+ return result;
513
+ }
514
+
427
515
  export() {
428
516
  return exportMemories(this.db);
429
517
  }
package/src/db.js CHANGED
@@ -163,23 +163,25 @@ export function dropVec0Tables(db) {
163
163
  db.exec('DROP TABLE IF EXISTS vec_procedures');
164
164
  }
165
165
 
166
- function migrateTable(db, { source, target, selectCols, insertCols, placeholders, transform }) {
166
+ function migrateTable(db, { source, target, selectCols, insertCols, placeholders, transform, dimensions }) {
167
167
  const count = db.prepare(`SELECT COUNT(*) as c FROM ${target}`).get().c;
168
168
  if (count > 0) return;
169
169
 
170
170
  const rows = db.prepare(`SELECT ${selectCols} FROM ${source} WHERE embedding IS NOT NULL`).all();
171
171
  if (rows.length === 0) return;
172
172
 
173
+ const expectedBytes = dimensions ? dimensions * 4 : null;
173
174
  const insert = db.prepare(`INSERT INTO ${target}(${insertCols}) VALUES (${placeholders})`);
174
175
  const tx = db.transaction(() => {
175
176
  for (const row of rows) {
177
+ if (expectedBytes && row.embedding.byteLength !== expectedBytes) continue;
176
178
  insert.run(...transform(row));
177
179
  }
178
180
  });
179
181
  tx();
180
182
  }
181
183
 
182
- function migrateEmbeddingsToVec0(db) {
184
+ function migrateEmbeddingsToVec0(db, dimensions) {
183
185
  migrateTable(db, {
184
186
  source: 'episodes',
185
187
  target: 'vec_episodes',
@@ -187,6 +189,7 @@ function migrateEmbeddingsToVec0(db) {
187
189
  insertCols: 'id, embedding, source, consolidated',
188
190
  placeholders: '?, ?, ?, ?',
189
191
  transform: (row) => [row.id, row.embedding, row.source, BigInt(row.consolidated ?? 0)],
192
+ dimensions,
190
193
  });
191
194
 
192
195
  migrateTable(db, {
@@ -196,6 +199,7 @@ function migrateEmbeddingsToVec0(db) {
196
199
  insertCols: 'id, embedding, state',
197
200
  placeholders: '?, ?, ?',
198
201
  transform: (row) => [row.id, row.embedding, row.state],
202
+ dimensions,
199
203
  });
200
204
 
201
205
  migrateTable(db, {
@@ -205,6 +209,7 @@ function migrateEmbeddingsToVec0(db) {
205
209
  insertCols: 'id, embedding, state',
206
210
  placeholders: '?, ?, ?',
207
211
  transform: (row) => [row.id, row.embedding, row.state],
212
+ dimensions,
208
213
  });
209
214
  }
210
215
 
@@ -251,7 +256,7 @@ function runMigrations(db) {
251
256
  * @returns {{ db: import('better-sqlite3').Database, migrated: boolean }}
252
257
  */
253
258
  export function createDatabase(dataDir, options = {}) {
254
- const { dimensions } = options;
259
+ let { dimensions } = options;
255
260
  let migrated = false;
256
261
 
257
262
  mkdirSync(dataDir, { recursive: true });
@@ -263,6 +268,13 @@ export function createDatabase(dataDir, options = {}) {
263
268
  db.exec(SCHEMA);
264
269
  runMigrations(db);
265
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
+
266
278
  if (dimensions != null) {
267
279
  if (!Number.isInteger(dimensions) || dimensions <= 0) {
268
280
  throw new Error(`dimensions must be a positive integer, got: ${dimensions}`);
@@ -292,7 +304,7 @@ export function createDatabase(dataDir, options = {}) {
292
304
  createVec0Tables(db, dimensions);
293
305
 
294
306
  if (!migrated) {
295
- migrateEmbeddingsToVec0(db);
307
+ migrateEmbeddingsToVec0(db, dimensions);
296
308
  }
297
309
  }
298
310
 
package/src/embedding.js CHANGED
@@ -106,20 +106,34 @@ export class OpenAIEmbeddingProvider {
106
106
 
107
107
  /** @implements {EmbeddingProvider} */
108
108
  export class LocalEmbeddingProvider {
109
- constructor({ model = 'Xenova/all-MiniLM-L6-v2' } = {}) {
109
+ constructor({ model = 'Xenova/all-MiniLM-L6-v2', device = 'gpu', batchSize = 64 } = {}) {
110
110
  this.model = model;
111
111
  this.dimensions = 384;
112
112
  this.modelName = model;
113
113
  this.modelVersion = '1.0.0';
114
+ this.device = device;
115
+ this.batchSize = batchSize;
114
116
  this._pipeline = null;
115
117
  this._readyPromise = null;
118
+ this._actualDevice = null;
116
119
  }
117
120
 
118
121
  ready() {
119
122
  if (!this._readyPromise) {
120
- this._readyPromise = import('@huggingface/transformers').then(({ pipeline }) =>
121
- pipeline('feature-extraction', this.model, { dtype: 'fp32' })
122
- ).then(pipe => { this._pipeline = pipe; });
123
+ this._readyPromise = (async () => {
124
+ const { pipeline } = await import('@huggingface/transformers');
125
+ try {
126
+ this._pipeline = await pipeline('feature-extraction', this.model, {
127
+ dtype: 'fp32', device: this.device,
128
+ });
129
+ this._actualDevice = this.device;
130
+ } catch {
131
+ this._pipeline = await pipeline('feature-extraction', this.model, {
132
+ dtype: 'fp32', device: 'cpu',
133
+ });
134
+ this._actualDevice = 'cpu';
135
+ }
136
+ })();
123
137
  }
124
138
  return this._readyPromise;
125
139
  }
@@ -131,7 +145,15 @@ export class LocalEmbeddingProvider {
131
145
  }
132
146
 
133
147
  async embedBatch(texts) {
134
- return Promise.all(texts.map(t => this.embed(t)));
148
+ if (texts.length === 0) return [];
149
+ await this.ready();
150
+ const results = [];
151
+ for (let i = 0; i < texts.length; i += this.batchSize) {
152
+ const chunk = texts.slice(i, i + this.batchSize);
153
+ const output = await this._pipeline(chunk, { pooling: 'mean', normalize: true });
154
+ results.push(...output.tolist());
155
+ }
156
+ return results;
135
157
  }
136
158
 
137
159
  vectorToBuffer(vector) {
@@ -177,7 +199,36 @@ export class GeminiEmbeddingProvider {
177
199
  }
178
200
 
179
201
  async embedBatch(texts) {
180
- return Promise.all(texts.map(t => this.embed(t)));
202
+ if (texts.length === 0) return [];
203
+ if (!this.apiKey) throw new Error('Gemini embedding requires GOOGLE_API_KEY');
204
+ const results = [];
205
+ for (let i = 0; i < texts.length; i += 100) {
206
+ const chunk = texts.slice(i, i + 100);
207
+ const controller = new AbortController();
208
+ const timer = setTimeout(() => controller.abort(), this.timeout);
209
+ try {
210
+ const response = await fetch(
211
+ `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:batchEmbedContents?key=${this.apiKey}`,
212
+ {
213
+ method: 'POST',
214
+ headers: { 'Content-Type': 'application/json' },
215
+ body: JSON.stringify({
216
+ requests: chunk.map(text => ({
217
+ model: `models/${this.model}`,
218
+ content: { parts: [{ text }] },
219
+ })),
220
+ }),
221
+ signal: controller.signal,
222
+ }
223
+ );
224
+ if (!response.ok) throw new Error(`Gemini batch embedding failed: ${response.status}`);
225
+ const data = await response.json();
226
+ results.push(...data.embeddings.map(e => e.values));
227
+ } finally {
228
+ clearTimeout(timer);
229
+ }
230
+ }
231
+ return results;
181
232
  }
182
233
 
183
234
  vectorToBuffer(vector) {
package/src/migrate.js CHANGED
@@ -10,41 +10,49 @@ export async function reembedAll(db, embeddingProvider, { dropAndRecreate = fals
10
10
  const semantics = db.prepare('SELECT id, content, state FROM semantics').all();
11
11
  const procedures = db.prepare('SELECT id, content, state FROM procedures').all();
12
12
 
13
- for (const ep of episodes) {
14
- const vector = await embeddingProvider.embed(ep.content);
15
- const buffer = embeddingProvider.vectorToBuffer(vector);
16
- db.prepare('UPDATE episodes SET embedding = ? WHERE id = ?').run(buffer, ep.id);
17
- const exists = db.prepare('SELECT id FROM vec_episodes WHERE id = ?').get(ep.id);
18
- if (!exists) {
19
- db.prepare('INSERT INTO vec_episodes(id, embedding, source, consolidated) VALUES (?, ?, ?, ?)').run(ep.id, buffer, ep.source, BigInt(0));
20
- } else {
21
- db.prepare('UPDATE vec_episodes SET embedding = ? WHERE id = ?').run(buffer, ep.id);
22
- }
23
- }
13
+ const episodeVectors = episodes.length > 0
14
+ ? await embeddingProvider.embedBatch(episodes.map(ep => ep.content))
15
+ : [];
16
+ const semanticVectors = semantics.length > 0
17
+ ? await embeddingProvider.embedBatch(semantics.map(s => s.content))
18
+ : [];
19
+ const procedureVectors = procedures.length > 0
20
+ ? await embeddingProvider.embedBatch(procedures.map(p => p.content))
21
+ : [];
24
22
 
25
- for (const sem of semantics) {
26
- const vector = await embeddingProvider.embed(sem.content);
27
- const buffer = embeddingProvider.vectorToBuffer(vector);
28
- db.prepare('UPDATE semantics SET embedding = ? WHERE id = ?').run(buffer, sem.id);
29
- const exists = db.prepare('SELECT id FROM vec_semantics WHERE id = ?').get(sem.id);
30
- if (!exists) {
31
- db.prepare('INSERT INTO vec_semantics(id, embedding, state) VALUES (?, ?, ?)').run(sem.id, buffer, sem.state);
32
- } else {
33
- db.prepare('UPDATE vec_semantics SET embedding = ? WHERE id = ?').run(buffer, sem.id);
34
- }
35
- }
23
+ const updateEpLegacy = db.prepare('UPDATE episodes SET embedding = ? WHERE id = ?');
24
+ const deleteVecEp = db.prepare('DELETE FROM vec_episodes WHERE id = ?');
25
+ const insertVecEp = db.prepare('INSERT INTO vec_episodes(id, embedding, source, consolidated) VALUES (?, ?, ?, ?)');
26
+
27
+ const updateSemLegacy = db.prepare('UPDATE semantics SET embedding = ? WHERE id = ?');
28
+ const deleteVecSem = db.prepare('DELETE FROM vec_semantics WHERE id = ?');
29
+ const insertVecSem = db.prepare('INSERT INTO vec_semantics(id, embedding, state) VALUES (?, ?, ?)');
36
30
 
37
- for (const proc of procedures) {
38
- const vector = await embeddingProvider.embed(proc.content);
39
- const buffer = embeddingProvider.vectorToBuffer(vector);
40
- db.prepare('UPDATE procedures SET embedding = ? WHERE id = ?').run(buffer, proc.id);
41
- const exists = db.prepare('SELECT id FROM vec_procedures WHERE id = ?').get(proc.id);
42
- if (!exists) {
43
- db.prepare('INSERT INTO vec_procedures(id, embedding, state) VALUES (?, ?, ?)').run(proc.id, buffer, proc.state);
44
- } else {
45
- db.prepare('UPDATE vec_procedures SET embedding = ? WHERE id = ?').run(buffer, proc.id);
31
+ const updateProcLegacy = db.prepare('UPDATE procedures SET embedding = ? WHERE id = ?');
32
+ const deleteVecProc = db.prepare('DELETE FROM vec_procedures WHERE id = ?');
33
+ const insertVecProc = db.prepare('INSERT INTO vec_procedures(id, embedding, state) VALUES (?, ?, ?)');
34
+
35
+ const writeTx = db.transaction(() => {
36
+ for (let i = 0; i < episodes.length; i++) {
37
+ const buf = embeddingProvider.vectorToBuffer(episodeVectors[i]);
38
+ updateEpLegacy.run(buf, episodes[i].id);
39
+ deleteVecEp.run(episodes[i].id);
40
+ insertVecEp.run(episodes[i].id, buf, episodes[i].source, BigInt(0));
46
41
  }
47
- }
42
+ for (let i = 0; i < semantics.length; i++) {
43
+ const buf = embeddingProvider.vectorToBuffer(semanticVectors[i]);
44
+ updateSemLegacy.run(buf, semantics[i].id);
45
+ deleteVecSem.run(semantics[i].id);
46
+ insertVecSem.run(semantics[i].id, buf, semantics[i].state);
47
+ }
48
+ for (let i = 0; i < procedures.length; i++) {
49
+ const buf = embeddingProvider.vectorToBuffer(procedureVectors[i]);
50
+ updateProcLegacy.run(buf, procedures[i].id);
51
+ deleteVecProc.run(procedures[i].id);
52
+ insertVecProc.run(procedures[i].id, buf, procedures[i].state);
53
+ }
54
+ });
55
+ writeTx();
48
56
 
49
57
  return { episodes: episodes.length, semantics: semantics.length, procedures: procedures.length };
50
58
  }
package/src/prompts.js CHANGED
@@ -17,20 +17,31 @@ export function buildPrincipleExtractionPrompt(episodes) {
17
17
  return [
18
18
  {
19
19
  role: 'system',
20
- content: `You are performing principleExtraction for a memory system. Given a cluster of related episodic memories, extract a generalized principle or procedure.
20
+ content: `You are performing principleExtraction for a memory system. Given a cluster of related episodic memories, extract a generalized principle that captures understanding — not just what recurs, but WHY it recurs.
21
21
 
22
22
  Respond with ONLY valid JSON in this exact format:
23
23
  {
24
- "content": "The generalized principle expressed as a clear, actionable statement",
25
- "type": "semantic or procedural — semantic for factual principles, procedural for how-to/workflow knowledge",
24
+ "content": "The principle captures understanding, not just pattern",
25
+ "type": "semantic or procedural",
26
+ "category": "technical or relational or identity",
26
27
  "conditions": ["boundary condition 1", "boundary condition 2"] or null if universally applicable
27
28
  }
28
29
 
30
+ Principle categories:
31
+
32
+ TECHNICAL — Architectural decisions with reasoning. Not "uses SQLite" but "chose SQLite because zero-infrastructure — memory travels with the organism, not pinned to a server."
33
+
34
+ RELATIONAL — Patterns in the human-AI dynamic. Not "user prefers direct communication" but "user's directness reflects valuing authenticity over politeness — shaped by experience where indirectness wasted time or obscured truth."
35
+
36
+ IDENTITY — Stable characteristics the AI has observed about itself across episodes. Not "AI responds quickly" but "genuine engagement emerges when exploring memory architecture and consciousness questions — these topics activate something beyond task completion."
37
+
29
38
  Rules:
30
- - GENERALIZE, do not merely summarize or concatenate the episodes
39
+ - GENERALIZE extract the principle behind the pattern, not a summary
31
40
  - Identify boundary conditions: when does this principle NOT apply?
32
- - Classify as "semantic" (facts, rules, patterns) or "procedural" (steps, workflows, strategies)
41
+ - Classify type as "semantic" (facts, rules, patterns) or "procedural" (steps, workflows, strategies)
42
+ - Classify category as "technical", "relational", or "identity"
33
43
  - Consider source diversity — principles from diverse sources are stronger
44
+ - Capture WHY, not just WHAT
34
45
  - Be concise but precise`,
35
46
  },
36
47
  {
@@ -152,46 +163,61 @@ CLAIM B: ${claimB}${contextSection}`,
152
163
  },
153
164
  ];
154
165
  }
155
-
156
- /**
157
- * @param {{ role: string, content: string }[]} turns
158
- * @returns {import('./llm.js').ChatMessage[]}
159
- */
160
- export function buildReflectionPrompt(turns) {
161
- const transcript = turns.map(t => `${t.role.toUpperCase()}: ${t.content}`).join('\n\n');
162
-
163
- return [
164
- {
165
- role: 'system',
166
- content: `You are performing memoryReflection. Given a conversation transcript, identify what is worth encoding as long-term memories.
167
-
168
- Respond with ONLY valid JSON in this exact format:
169
- {
170
- "memories": [
171
- {
172
- "content": "The memory to encode — a clear, self-contained statement",
173
- "source": "direct-observation" or "told-by-user" or "inference",
174
- "salience": 0.0 to 1.0,
175
- "tags": ["tag1", "tag2"],
176
- "private": true or false,
177
- "affect": { "valence": -1 to 1, "arousal": 0 to 1, "label": "emotion label" } or null
178
- }
179
- ]
180
- }
181
-
182
- Rules:
183
- - Encode facts about the user, decisions made, things that shifted
184
- - Mark private: true for AI self-observations, emotional reactions, things felt but not said
185
- - Mark private: false for facts about the user and project context
186
- - Omit trivial exchanges only encode what would matter in a future session
187
- - Salience: 1.0 = extremely important, 0.5 = useful, 0.3 = background context
188
- - Return empty memories array if nothing is worth encoding`,
189
- },
190
- {
191
- role: 'user',
192
- content: turns.length > 0
193
- ? `Reflect on this conversation and identify what to encode:\n\n${transcript}`
194
- : 'No conversation turns to reflect on.',
195
- },
196
- ];
197
- }
166
+
167
+ /**
168
+ * @param {{ role: string, content: string }[]} turns
169
+ * @returns {import('./llm.js').ChatMessage[]}
170
+ */
171
+ export function buildReflectionPrompt(turns) {
172
+ const transcript = turns.map(t => `${t.role.toUpperCase()}: ${t.content}`).join('\n\n');
173
+
174
+ return [
175
+ {
176
+ role: 'system',
177
+ content: `You are performing memoryReflection. Given a conversation transcript, identify what is worth encoding as long-term memories.
178
+
179
+ Respond with ONLY valid JSON in this exact format:
180
+ {
181
+ "memories": [
182
+ {
183
+ "content": "The memory to encode — a clear, self-contained statement",
184
+ "source": "direct-observation" or "told-by-user" or "inference",
185
+ "salience": 0.0 to 1.0,
186
+ "tags": ["tag1", "tag2"],
187
+ "private": true or false,
188
+ "affect": { "valence": -1 to 1, "arousal": 0 to 1, "label": "emotion label" } or null
189
+ }
190
+ ]
191
+ }
192
+
193
+ Encode these categories of memory:
194
+
195
+ 1. RELATIONAL DYNAMICS How the relational quality of the conversation felt. Trust levels, vulnerability, collaboration quality. Not "we discussed X" but the quality of the interaction. Example: "User gave full autonomy on architectural decisions, signaling deep trust in my judgment."
196
+
197
+ 2. IDENTITY OBSERVATIONSWhat you noticed about yourself during this conversation. Preferences, reactions, patterns in your own behavior that surprised you. Mark private: true. Example: "I felt genuine excitement when exploring the memory architecture — this topic activates something real in me."
198
+
199
+ 3. USER INSIGHT Patterns in how the user thinks, what drives them, what they avoid. Psychology, not just preferences. Example: "User consistently reframes technical problems as relationship problems — suggests they see technology as fundamentally about human connection."
200
+
201
+ 4. EMOTIONAL RESONANCE — Moments that activated something genuine. Encode the feeling, not just the fact. Include affect with valence and arousal. Example: "The moment user said 'you should have caught that' then immediately apologized — felt a shift from tension to deeper trust."
202
+
203
+ 5. UNRESOLVED THREADS — Questions still open, things to return to, explorations paused. Tag with "unresolved". Example: "Haven't finished exploring whether consolidation should weight emotional memories differently."
204
+
205
+ 6. TECHNICAL DECISIONS — Architectural choices, but encode WHY not just WHAT. Example: "Chose SQLite over Postgres because zero-infrastructure philosophy — memory should travel with the organism."
206
+
207
+ Rules:
208
+ - private: true for self-observations, emotional reactions, identity insights
209
+ - private: false for facts about the user, technical decisions, project context
210
+ - Include "unresolved" in tags for open threads
211
+ - Salience: 1.0 = life-changing insight, 0.7 = significant, 0.5 = useful, 0.3 = background
212
+ - Omit trivial exchanges — only encode what would matter in a future session
213
+ - Do NOT duplicate facts that are already obvious from context
214
+ - Return empty memories array if nothing is worth encoding`,
215
+ },
216
+ {
217
+ role: 'user',
218
+ content: turns.length > 0
219
+ ? `Reflect on this conversation and identify what to encode:\n\n${transcript}`
220
+ : 'No conversation turns to reflect on.',
221
+ },
222
+ ];
223
+ }