audrey 0.9.0 → 0.11.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.
package/src/embedding.js CHANGED
@@ -11,33 +11,14 @@ import { createHash } from 'node:crypto';
11
11
  * @property {(buffer: Buffer) => number[]} bufferToVector
12
12
  */
13
13
 
14
- /**
15
- * @typedef {Object} MockEmbeddingConfig
16
- * @property {'mock'} provider
17
- * @property {number} [dimensions=64]
18
- */
19
-
20
- /**
21
- * @typedef {Object} OpenAIEmbeddingConfig
22
- * @property {'openai'} provider
23
- * @property {string} [apiKey]
24
- * @property {string} [model='text-embedding-3-small']
25
- * @property {number} [dimensions=1536]
26
- */
27
-
28
14
  /** @implements {EmbeddingProvider} */
29
15
  export class MockEmbeddingProvider {
30
- /** @param {Partial<MockEmbeddingConfig>} [config={}] */
31
16
  constructor({ dimensions = 64 } = {}) {
32
17
  this.dimensions = dimensions;
33
18
  this.modelName = 'mock-embedding';
34
19
  this.modelVersion = '1.0.0';
35
20
  }
36
21
 
37
- /**
38
- * @param {string} text
39
- * @returns {Promise<number[]>}
40
- */
41
22
  async embed(text) {
42
23
  const hash = createHash('sha256').update(text).digest();
43
24
  const vector = new Array(this.dimensions);
@@ -48,26 +29,14 @@ export class MockEmbeddingProvider {
48
29
  return vector.map(v => v / magnitude);
49
30
  }
50
31
 
51
- /**
52
- * @param {string[]} texts
53
- * @returns {Promise<number[][]>}
54
- */
55
32
  async embedBatch(texts) {
56
33
  return Promise.all(texts.map(t => this.embed(t)));
57
34
  }
58
35
 
59
- /**
60
- * @param {number[]} vector
61
- * @returns {Buffer}
62
- */
63
36
  vectorToBuffer(vector) {
64
37
  return Buffer.from(new Float32Array(vector).buffer);
65
38
  }
66
39
 
67
- /**
68
- * @param {Buffer} buffer
69
- * @returns {number[]}
70
- */
71
40
  bufferToVector(buffer) {
72
41
  return Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
73
42
  }
@@ -75,7 +44,6 @@ export class MockEmbeddingProvider {
75
44
 
76
45
  /** @implements {EmbeddingProvider} */
77
46
  export class OpenAIEmbeddingProvider {
78
- /** @param {Partial<OpenAIEmbeddingConfig>} [config={}] */
79
47
  constructor({ apiKey, model = 'text-embedding-3-small', dimensions = 1536, timeout = 30000 } = {}) {
80
48
  this.apiKey = apiKey || process.env.OPENAI_API_KEY;
81
49
  this.model = model;
@@ -85,10 +53,6 @@ export class OpenAIEmbeddingProvider {
85
53
  this.modelVersion = 'latest';
86
54
  }
87
55
 
88
- /**
89
- * @param {string} text
90
- * @returns {Promise<number[]>}
91
- */
92
56
  async embed(text) {
93
57
  const controller = new AbortController();
94
58
  const timer = setTimeout(() => controller.abort(), this.timeout);
@@ -110,10 +74,6 @@ export class OpenAIEmbeddingProvider {
110
74
  }
111
75
  }
112
76
 
113
- /**
114
- * @param {string[]} texts
115
- * @returns {Promise<number[][]>}
116
- */
117
77
  async embedBatch(texts) {
118
78
  const controller = new AbortController();
119
79
  const timer = setTimeout(() => controller.abort(), this.timeout);
@@ -135,34 +95,111 @@ export class OpenAIEmbeddingProvider {
135
95
  }
136
96
  }
137
97
 
138
- /**
139
- * @param {number[]} vector
140
- * @returns {Buffer}
141
- */
142
98
  vectorToBuffer(vector) {
143
99
  return Buffer.from(new Float32Array(vector).buffer);
144
100
  }
145
101
 
146
- /**
147
- * @param {Buffer} buffer
148
- * @returns {number[]}
149
- */
150
102
  bufferToVector(buffer) {
151
103
  return Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
152
104
  }
153
105
  }
154
106
 
155
- /**
156
- * @param {MockEmbeddingConfig | OpenAIEmbeddingConfig} config
157
- * @returns {MockEmbeddingProvider | OpenAIEmbeddingProvider}
158
- */
107
+ /** @implements {EmbeddingProvider} */
108
+ export class LocalEmbeddingProvider {
109
+ constructor({ model = 'Xenova/all-MiniLM-L6-v2' } = {}) {
110
+ this.model = model;
111
+ this.dimensions = 384;
112
+ this.modelName = model;
113
+ this.modelVersion = '1.0.0';
114
+ this._pipeline = null;
115
+ this._readyPromise = null;
116
+ }
117
+
118
+ ready() {
119
+ 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
+ }
124
+ return this._readyPromise;
125
+ }
126
+
127
+ async embed(text) {
128
+ await this.ready();
129
+ const output = await this._pipeline(text, { pooling: 'mean', normalize: true });
130
+ return Array.from(output.data);
131
+ }
132
+
133
+ async embedBatch(texts) {
134
+ return Promise.all(texts.map(t => this.embed(t)));
135
+ }
136
+
137
+ vectorToBuffer(vector) {
138
+ return Buffer.from(new Float32Array(vector).buffer);
139
+ }
140
+
141
+ bufferToVector(buffer) {
142
+ return Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
143
+ }
144
+ }
145
+
146
+ /** @implements {EmbeddingProvider} */
147
+ export class GeminiEmbeddingProvider {
148
+ constructor({ apiKey, model = 'gemini-embedding-001', timeout = 30000 } = {}) {
149
+ this.apiKey = apiKey || process.env.GOOGLE_API_KEY;
150
+ this.model = model;
151
+ this.dimensions = 3072;
152
+ this.timeout = timeout;
153
+ this.modelName = model;
154
+ this.modelVersion = 'latest';
155
+ }
156
+
157
+ async embed(text) {
158
+ if (!this.apiKey) throw new Error('Gemini embedding requires GOOGLE_API_KEY');
159
+ const controller = new AbortController();
160
+ const timer = setTimeout(() => controller.abort(), this.timeout);
161
+ try {
162
+ const response = await fetch(
163
+ `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:embedContent?key=${this.apiKey}`,
164
+ {
165
+ method: 'POST',
166
+ headers: { 'Content-Type': 'application/json' },
167
+ body: JSON.stringify({ model: `models/${this.model}`, content: { parts: [{ text }] } }),
168
+ signal: controller.signal,
169
+ }
170
+ );
171
+ if (!response.ok) throw new Error(`Gemini embedding failed: ${response.status}`);
172
+ const data = await response.json();
173
+ return data.embedding.values;
174
+ } finally {
175
+ clearTimeout(timer);
176
+ }
177
+ }
178
+
179
+ async embedBatch(texts) {
180
+ return Promise.all(texts.map(t => this.embed(t)));
181
+ }
182
+
183
+ vectorToBuffer(vector) {
184
+ return Buffer.from(new Float32Array(vector).buffer);
185
+ }
186
+
187
+ bufferToVector(buffer) {
188
+ return Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
189
+ }
190
+ }
191
+
159
192
  export function createEmbeddingProvider(config) {
160
193
  switch (config.provider) {
161
194
  case 'mock':
162
195
  return new MockEmbeddingProvider(config);
163
196
  case 'openai':
164
197
  return new OpenAIEmbeddingProvider(config);
198
+ case 'local':
199
+ return new LocalEmbeddingProvider(config);
200
+ case 'gemini':
201
+ return new GeminiEmbeddingProvider(config);
165
202
  default:
166
- throw new Error(`Unknown embedding provider: ${config.provider}. Valid: mock, openai`);
203
+ throw new Error(`Unknown embedding provider: ${config.provider}. Valid: mock, openai, local, gemini`);
167
204
  }
168
205
  }
package/src/encode.js CHANGED
@@ -1,61 +1,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 }} 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
- }) {
22
- if (!content || typeof content !== 'string') throw new Error('content must be a non-empty string');
23
- if (salience < 0 || salience > 1) throw new Error('salience must be between 0 and 1');
24
- if (tags && !Array.isArray(tags)) throw new Error('tags must be an array');
25
-
26
- const reliability = sourceReliability(source);
27
- const vector = await embeddingProvider.embed(content);
28
- const embeddingBuffer = embeddingProvider.vectorToBuffer(vector);
29
- const id = generateId();
30
- const now = new Date().toISOString();
31
-
32
- const boost = arousalSalienceBoost(affect.arousal);
33
- const effectiveSalience = Math.min(1.0, salience + (boost * arousalWeight));
34
-
35
- const insertAndLink = db.transaction(() => {
36
- db.prepare(`
37
- INSERT INTO episodes (
38
- id, content, embedding, source, source_reliability, salience, context, affect,
39
- tags, causal_trigger, causal_consequence, created_at,
40
- embedding_model, embedding_version, supersedes
41
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
42
- `).run(
43
- id, content, embeddingBuffer, source, reliability, effectiveSalience,
44
- JSON.stringify(context),
45
- JSON.stringify(affect),
46
- tags ? JSON.stringify(tags) : null,
47
- causal?.trigger || null, causal?.consequence || null,
48
- now, embeddingProvider.modelName, embeddingProvider.modelVersion,
49
- supersedes || null,
50
- );
51
- db.prepare(
52
- 'INSERT INTO vec_episodes(id, embedding, source, consolidated) VALUES (?, ?, ?, ?)'
53
- ).run(id, embeddingBuffer, source, BigInt(0));
54
- if (supersedes) {
55
- db.prepare('UPDATE episodes SET superseded_by = ? WHERE id = ?').run(id, supersedes);
56
- }
57
- });
58
-
59
- insertAndLink();
60
- return id;
61
- }
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
+ }
package/src/export.js CHANGED
@@ -8,21 +8,23 @@ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8')
8
8
 
9
9
  export function exportMemories(db) {
10
10
  const episodes = db.prepare(
11
- 'SELECT id, content, source, source_reliability, salience, tags, causal_trigger, causal_consequence, created_at, supersedes, superseded_by, consolidated FROM episodes'
11
+ 'SELECT id, content, source, source_reliability, salience, context, affect, tags, causal_trigger, causal_consequence, created_at, supersedes, superseded_by, consolidated, "private" FROM episodes'
12
12
  ).all().map(ep => ({
13
13
  ...ep,
14
14
  tags: safeJsonParse(ep.tags, null),
15
+ context: safeJsonParse(ep.context, null),
16
+ affect: safeJsonParse(ep.affect, null),
15
17
  }));
16
18
 
17
19
  const semantics = db.prepare(
18
- 'SELECT id, content, state, conditions, evidence_episode_ids, evidence_count, supporting_count, contradicting_count, source_type_diversity, consolidation_checkpoint, created_at, last_reinforced_at, retrieval_count, challenge_count FROM semantics'
20
+ 'SELECT id, content, state, conditions, evidence_episode_ids, evidence_count, supporting_count, contradicting_count, source_type_diversity, consolidation_checkpoint, created_at, last_reinforced_at, retrieval_count, challenge_count, interference_count, salience FROM semantics'
19
21
  ).all().map(sem => ({
20
22
  ...sem,
21
23
  evidence_episode_ids: safeJsonParse(sem.evidence_episode_ids, []),
22
24
  }));
23
25
 
24
26
  const procedures = db.prepare(
25
- 'SELECT id, content, state, trigger_conditions, evidence_episode_ids, success_count, failure_count, created_at, last_reinforced_at, retrieval_count FROM procedures'
27
+ 'SELECT id, content, state, trigger_conditions, evidence_episode_ids, success_count, failure_count, created_at, last_reinforced_at, retrieval_count, interference_count, salience FROM procedures'
26
28
  ).all().map(proc => ({
27
29
  ...proc,
28
30
  evidence_episode_ids: safeJsonParse(proc.evidence_episode_ids, []),
package/src/import.js CHANGED
@@ -5,9 +5,9 @@ export async function importMemories(db, embeddingProvider, snapshot) {
5
5
  }
6
6
 
7
7
  const insertEpisode = db.prepare(`
8
- INSERT INTO episodes (id, content, source, source_reliability, salience, tags,
9
- causal_trigger, causal_consequence, created_at, supersedes, superseded_by, consolidated)
10
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8
+ INSERT INTO episodes (id, content, source, source_reliability, salience, context, affect, tags,
9
+ causal_trigger, causal_consequence, created_at, supersedes, superseded_by, consolidated, "private")
10
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
11
11
  `);
12
12
 
13
13
  const insertVecEpisode = db.prepare(
@@ -17,8 +17,9 @@ export async function importMemories(db, embeddingProvider, snapshot) {
17
17
  const insertSemantic = db.prepare(`
18
18
  INSERT INTO semantics (id, content, state, conditions, evidence_episode_ids,
19
19
  evidence_count, supporting_count, contradicting_count, source_type_diversity,
20
- consolidation_checkpoint, created_at, last_reinforced_at, retrieval_count, challenge_count)
21
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
20
+ consolidation_checkpoint, created_at, last_reinforced_at, retrieval_count, challenge_count,
21
+ interference_count, salience)
22
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
22
23
  `);
23
24
 
24
25
  const insertVecSemantic = db.prepare(
@@ -27,8 +28,9 @@ export async function importMemories(db, embeddingProvider, snapshot) {
27
28
 
28
29
  const insertProcedure = db.prepare(`
29
30
  INSERT INTO procedures (id, content, state, trigger_conditions, evidence_episode_ids,
30
- success_count, failure_count, created_at, last_reinforced_at, retrieval_count)
31
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
31
+ success_count, failure_count, created_at, last_reinforced_at, retrieval_count,
32
+ interference_count, salience)
33
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
32
34
  `);
33
35
 
34
36
  const insertVecProcedure = db.prepare(
@@ -53,10 +55,13 @@ export async function importMemories(db, embeddingProvider, snapshot) {
53
55
 
54
56
  for (const ep of snapshot.episodes) {
55
57
  const tags = ep.tags ? JSON.stringify(ep.tags) : null;
58
+ const context = ep.context ? JSON.stringify(ep.context) : '{}';
59
+ const affect = ep.affect ? JSON.stringify(ep.affect) : '{}';
56
60
  insertEpisode.run(
57
61
  ep.id, ep.content, ep.source, ep.source_reliability, ep.salience ?? 0.5,
58
- tags, ep.causal_trigger ?? null, ep.causal_consequence ?? null,
62
+ context, affect, tags, ep.causal_trigger ?? null, ep.causal_consequence ?? null,
59
63
  ep.created_at, ep.supersedes ?? null, ep.superseded_by ?? null, ep.consolidated ?? 0,
64
+ ep.private ?? 0,
60
65
  );
61
66
 
62
67
  const vector = await embeddingProvider.embed(ep.content);
@@ -71,6 +76,7 @@ export async function importMemories(db, embeddingProvider, snapshot) {
71
76
  sem.evidence_count ?? 0, sem.supporting_count ?? 0, sem.contradicting_count ?? 0,
72
77
  sem.source_type_diversity ?? 0, sem.consolidation_checkpoint ?? null,
73
78
  sem.created_at, sem.last_reinforced_at ?? null, sem.retrieval_count ?? 0, sem.challenge_count ?? 0,
79
+ sem.interference_count ?? 0, sem.salience ?? 0.5,
74
80
  );
75
81
 
76
82
  const vector = await embeddingProvider.embed(sem.content);
@@ -84,6 +90,7 @@ export async function importMemories(db, embeddingProvider, snapshot) {
84
90
  JSON.stringify(proc.evidence_episode_ids || []),
85
91
  proc.success_count ?? 0, proc.failure_count ?? 0,
86
92
  proc.created_at, proc.last_reinforced_at ?? null, proc.retrieval_count ?? 0,
93
+ proc.interference_count ?? 0, proc.salience ?? 0.5,
87
94
  );
88
95
 
89
96
  const vector = await embeddingProvider.embed(proc.content);
package/src/migrate.js CHANGED
@@ -1,4 +1,11 @@
1
- export async function reembedAll(db, embeddingProvider) {
1
+ import { dropVec0Tables, createVec0Tables } from './db.js';
2
+
3
+ export async function reembedAll(db, embeddingProvider, { dropAndRecreate = false } = {}) {
4
+ if (dropAndRecreate) {
5
+ dropVec0Tables(db);
6
+ createVec0Tables(db, embeddingProvider.dimensions);
7
+ }
8
+
2
9
  const episodes = db.prepare('SELECT id, content, source FROM episodes').all();
3
10
  const semantics = db.prepare('SELECT id, content, state FROM semantics').all();
4
11
  const procedures = db.prepare('SELECT id, content, state FROM procedures').all();
@@ -7,26 +14,37 @@ export async function reembedAll(db, embeddingProvider) {
7
14
  const vector = await embeddingProvider.embed(ep.content);
8
15
  const buffer = embeddingProvider.vectorToBuffer(vector);
9
16
  db.prepare('UPDATE episodes SET embedding = ? WHERE id = ?').run(buffer, ep.id);
10
- db.prepare('INSERT INTO vec_episodes(id, embedding, source, consolidated) VALUES (?, ?, ?, ?)').run(ep.id, buffer, ep.source, BigInt(0));
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
+ }
11
23
  }
12
24
 
13
25
  for (const sem of semantics) {
14
26
  const vector = await embeddingProvider.embed(sem.content);
15
27
  const buffer = embeddingProvider.vectorToBuffer(vector);
16
28
  db.prepare('UPDATE semantics SET embedding = ? WHERE id = ?').run(buffer, sem.id);
17
- db.prepare('INSERT INTO vec_semantics(id, embedding, state) VALUES (?, ?, ?)').run(sem.id, buffer, sem.state);
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
+ }
18
35
  }
19
36
 
20
37
  for (const proc of procedures) {
21
38
  const vector = await embeddingProvider.embed(proc.content);
22
39
  const buffer = embeddingProvider.vectorToBuffer(vector);
23
40
  db.prepare('UPDATE procedures SET embedding = ? WHERE id = ?').run(buffer, proc.id);
24
- db.prepare('INSERT INTO vec_procedures(id, embedding, state) VALUES (?, ?, ?)').run(proc.id, buffer, proc.state);
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);
46
+ }
25
47
  }
26
48
 
27
- return {
28
- episodes: episodes.length,
29
- semantics: semantics.length,
30
- procedures: procedures.length,
31
- };
49
+ return { episodes: episodes.length, semantics: semantics.length, procedures: procedures.length };
32
50
  }
package/src/prompts.js CHANGED
@@ -152,3 +152,46 @@ CLAIM B: ${claimB}${contextSection}`,
152
152
  },
153
153
  ];
154
154
  }
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
+ }
package/src/recall.js CHANGED
@@ -150,7 +150,8 @@ function matchesDateFilters(createdAt, filters) {
150
150
  return true;
151
151
  }
152
152
 
153
- function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters = {}) {
153
+ function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters = {}, includePrivate = false) {
154
+ const privateClause = includePrivate ? '' : 'AND e."private" = 0';
154
155
  const rows = db.prepare(`
155
156
  SELECT e.*, (1.0 - v.distance) AS similarity
156
157
  FROM vec_episodes v
@@ -158,6 +159,7 @@ function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includePro
158
159
  WHERE v.embedding MATCH ?
159
160
  AND k = ?
160
161
  AND e.superseded_by IS NULL
162
+ ${privateClause}
161
163
  `).all(queryBuffer, candidateK);
162
164
 
163
165
  const results = [];
@@ -258,6 +260,7 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
258
260
  sources,
259
261
  after,
260
262
  before,
263
+ includePrivate = false,
261
264
  } = options;
262
265
 
263
266
  const queryVector = await embeddingProvider.embed(query);
@@ -271,7 +274,7 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
271
274
  const allResults = [];
272
275
 
273
276
  if (searchTypes.includes('episodic')) {
274
- const episodic = knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters);
277
+ const episodic = knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters, includePrivate);
275
278
  allResults.push(...episodic);
276
279
  }
277
280
 
@@ -281,13 +284,11 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
281
284
  allResults.push(...semResults);
282
285
 
283
286
  if (semIds.length > 0) {
284
- const updateStmt = db.prepare(
285
- 'UPDATE semantics SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id = ?'
286
- );
287
287
  const nowISO = now.toISOString();
288
- for (const id of semIds) {
289
- updateStmt.run(nowISO, id);
290
- }
288
+ const placeholders = semIds.map(() => '?').join(',');
289
+ db.prepare(
290
+ `UPDATE semantics SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id IN (${placeholders})`
291
+ ).run(nowISO, ...semIds);
291
292
  }
292
293
  }
293
294
 
@@ -297,13 +298,11 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
297
298
  allResults.push(...procResults);
298
299
 
299
300
  if (procIds.length > 0) {
300
- const updateStmt = db.prepare(
301
- 'UPDATE procedures SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id = ?'
302
- );
303
301
  const nowISO = now.toISOString();
304
- for (const id of procIds) {
305
- updateStmt.run(nowISO, id);
306
- }
302
+ const placeholders = procIds.map(() => '?').join(',');
303
+ db.prepare(
304
+ `UPDATE procedures SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id IN (${placeholders})`
305
+ ).run(nowISO, ...procIds);
307
306
  }
308
307
  }
309
308