audrey 0.9.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.
package/src/db.js CHANGED
@@ -1,282 +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
- FOREIGN KEY (supersedes) REFERENCES episodes(id)
26
- );
27
-
28
- CREATE TABLE IF NOT EXISTS semantics (
29
- id TEXT PRIMARY KEY,
30
- content TEXT NOT NULL,
31
- embedding BLOB,
32
- state TEXT DEFAULT 'active' CHECK(state IN ('active','disputed','superseded','context_dependent','dormant','rolled_back')),
33
- conditions TEXT,
34
- evidence_episode_ids TEXT,
35
- evidence_count INTEGER DEFAULT 0,
36
- supporting_count INTEGER DEFAULT 0,
37
- contradicting_count INTEGER DEFAULT 0,
38
- source_type_diversity INTEGER DEFAULT 0,
39
- consolidation_checkpoint TEXT,
40
- embedding_model TEXT,
41
- embedding_version TEXT,
42
- consolidation_model TEXT,
43
- consolidation_prompt_hash TEXT,
44
- created_at TEXT NOT NULL,
45
- last_reinforced_at TEXT,
46
- retrieval_count INTEGER DEFAULT 0,
47
- challenge_count INTEGER DEFAULT 0,
48
- interference_count INTEGER DEFAULT 0,
49
- salience REAL DEFAULT 0.5
50
- );
51
-
52
- CREATE TABLE IF NOT EXISTS procedures (
53
- id TEXT PRIMARY KEY,
54
- content TEXT NOT NULL,
55
- embedding BLOB,
56
- state TEXT DEFAULT 'active' CHECK(state IN ('active','disputed','superseded','context_dependent','dormant','rolled_back')),
57
- trigger_conditions TEXT,
58
- evidence_episode_ids TEXT,
59
- success_count INTEGER DEFAULT 0,
60
- failure_count INTEGER DEFAULT 0,
61
- embedding_model TEXT,
62
- embedding_version TEXT,
63
- created_at TEXT NOT NULL,
64
- last_reinforced_at TEXT,
65
- retrieval_count INTEGER DEFAULT 0,
66
- interference_count INTEGER DEFAULT 0,
67
- salience REAL DEFAULT 0.5
68
- );
69
-
70
- CREATE TABLE IF NOT EXISTS causal_links (
71
- id TEXT PRIMARY KEY,
72
- cause_id TEXT NOT NULL,
73
- effect_id TEXT NOT NULL,
74
- link_type TEXT DEFAULT 'causal' CHECK(link_type IN ('causal','correlational','temporal')),
75
- mechanism TEXT,
76
- confidence REAL,
77
- evidence_count INTEGER DEFAULT 1,
78
- created_at TEXT NOT NULL
79
- );
80
-
81
- CREATE TABLE IF NOT EXISTS contradictions (
82
- id TEXT PRIMARY KEY,
83
- claim_a_id TEXT NOT NULL,
84
- claim_b_id TEXT NOT NULL,
85
- claim_a_type TEXT NOT NULL,
86
- claim_b_type TEXT NOT NULL,
87
- state TEXT DEFAULT 'open' CHECK(state IN ('open','resolved','context_dependent','reopened')),
88
- resolution TEXT,
89
- resolved_at TEXT,
90
- reopened_at TEXT,
91
- reopen_evidence_id TEXT,
92
- created_at TEXT NOT NULL
93
- );
94
-
95
- CREATE TABLE IF NOT EXISTS consolidation_runs (
96
- id TEXT PRIMARY KEY,
97
- checkpoint_cursor TEXT,
98
- input_episode_ids TEXT,
99
- output_memory_ids TEXT,
100
- confidence_deltas TEXT,
101
- consolidation_model TEXT,
102
- consolidation_prompt_hash TEXT,
103
- started_at TEXT,
104
- completed_at TEXT,
105
- status TEXT DEFAULT 'running' CHECK(status IN ('running','completed','failed','rolled_back'))
106
- );
107
-
108
- CREATE TABLE IF NOT EXISTS audrey_config (
109
- key TEXT PRIMARY KEY,
110
- value TEXT NOT NULL
111
- );
112
-
113
- CREATE TABLE IF NOT EXISTS consolidation_metrics (
114
- id TEXT PRIMARY KEY,
115
- run_id TEXT NOT NULL,
116
- min_cluster_size INTEGER NOT NULL,
117
- similarity_threshold REAL NOT NULL,
118
- episodes_evaluated INTEGER NOT NULL,
119
- clusters_found INTEGER NOT NULL,
120
- principles_extracted INTEGER NOT NULL,
121
- created_at TEXT NOT NULL,
122
- FOREIGN KEY (run_id) REFERENCES consolidation_runs(id)
123
- );
124
-
125
- CREATE INDEX IF NOT EXISTS idx_episodes_created ON episodes(created_at);
126
- CREATE INDEX IF NOT EXISTS idx_episodes_consolidated ON episodes(consolidated);
127
- CREATE INDEX IF NOT EXISTS idx_episodes_source ON episodes(source);
128
- CREATE INDEX IF NOT EXISTS idx_semantics_state ON semantics(state);
129
- CREATE INDEX IF NOT EXISTS idx_procedures_state ON procedures(state);
130
- CREATE INDEX IF NOT EXISTS idx_contradictions_state ON contradictions(state);
131
- CREATE INDEX IF NOT EXISTS idx_consolidation_status ON consolidation_runs(status);
132
- `;
133
-
134
- function createVec0Tables(db, dimensions) {
135
- db.exec(`
136
- CREATE VIRTUAL TABLE IF NOT EXISTS vec_episodes USING vec0(
137
- id text primary key,
138
- embedding float[${dimensions}] distance_metric=cosine,
139
- source text,
140
- consolidated integer
141
- );
142
- `);
143
- db.exec(`
144
- CREATE VIRTUAL TABLE IF NOT EXISTS vec_semantics USING vec0(
145
- id text primary key,
146
- embedding float[${dimensions}] distance_metric=cosine,
147
- state text
148
- );
149
- `);
150
- db.exec(`
151
- CREATE VIRTUAL TABLE IF NOT EXISTS vec_procedures USING vec0(
152
- id text primary key,
153
- embedding float[${dimensions}] distance_metric=cosine,
154
- state text
155
- );
156
- `);
157
- }
158
-
159
- function dropVec0Tables(db) {
160
- db.exec('DROP TABLE IF EXISTS vec_episodes');
161
- db.exec('DROP TABLE IF EXISTS vec_semantics');
162
- db.exec('DROP TABLE IF EXISTS vec_procedures');
163
- }
164
-
165
- function migrateTable(db, { source, target, selectCols, insertCols, placeholders, transform }) {
166
- const count = db.prepare(`SELECT COUNT(*) as c FROM ${target}`).get().c;
167
- if (count > 0) return;
168
-
169
- const rows = db.prepare(`SELECT ${selectCols} FROM ${source} WHERE embedding IS NOT NULL`).all();
170
- if (rows.length === 0) return;
171
-
172
- const insert = db.prepare(`INSERT INTO ${target}(${insertCols}) VALUES (${placeholders})`);
173
- const tx = db.transaction(() => {
174
- for (const row of rows) {
175
- insert.run(...transform(row));
176
- }
177
- });
178
- tx();
179
- }
180
-
181
- function migrateEmbeddingsToVec0(db) {
182
- migrateTable(db, {
183
- source: 'episodes',
184
- target: 'vec_episodes',
185
- selectCols: 'id, embedding, source, consolidated',
186
- insertCols: 'id, embedding, source, consolidated',
187
- placeholders: '?, ?, ?, ?',
188
- transform: (row) => [row.id, row.embedding, row.source, BigInt(row.consolidated ?? 0)],
189
- });
190
-
191
- migrateTable(db, {
192
- source: 'semantics',
193
- target: 'vec_semantics',
194
- selectCols: 'id, embedding, state',
195
- insertCols: 'id, embedding, state',
196
- placeholders: '?, ?, ?',
197
- transform: (row) => [row.id, row.embedding, row.state],
198
- });
199
-
200
- migrateTable(db, {
201
- source: 'procedures',
202
- target: 'vec_procedures',
203
- selectCols: 'id, embedding, state',
204
- insertCols: 'id, embedding, state',
205
- placeholders: '?, ?, ?',
206
- transform: (row) => [row.id, row.embedding, row.state],
207
- });
208
- }
209
-
210
- /**
211
- * @param {string} dataDir
212
- * @param {{ dimensions?: number }} [options]
213
- * @returns {{ db: import('better-sqlite3').Database, migrated: boolean }}
214
- */
215
- export function createDatabase(dataDir, options = {}) {
216
- const { dimensions } = options;
217
- let migrated = false;
218
-
219
- mkdirSync(dataDir, { recursive: true });
220
- const dbPath = join(dataDir, 'audrey.db');
221
- const db = new Database(dbPath);
222
- db.pragma('journal_mode = WAL');
223
- db.pragma('foreign_keys = ON');
224
- db.pragma('busy_timeout = 5000');
225
- db.exec(SCHEMA);
226
-
227
- if (dimensions != null) {
228
- if (!Number.isInteger(dimensions) || dimensions <= 0) {
229
- throw new Error(`dimensions must be a positive integer, got: ${dimensions}`);
230
- }
231
-
232
- sqliteVec.load(db);
233
-
234
- const existing = db.prepare(
235
- "SELECT value FROM audrey_config WHERE key = 'dimensions'"
236
- ).get();
237
-
238
- if (existing) {
239
- const storedDims = parseInt(existing.value, 10);
240
- if (storedDims !== dimensions) {
241
- dropVec0Tables(db);
242
- db.prepare(
243
- "UPDATE audrey_config SET value = ? WHERE key = 'dimensions'"
244
- ).run(String(dimensions));
245
- migrated = true;
246
- }
247
- } else {
248
- db.prepare(
249
- "INSERT INTO audrey_config (key, value) VALUES ('dimensions', ?)"
250
- ).run(String(dimensions));
251
- }
252
-
253
- createVec0Tables(db, dimensions);
254
-
255
- if (!migrated) {
256
- migrateEmbeddingsToVec0(db);
257
- }
258
- }
259
-
260
- return { db, migrated };
261
- }
262
-
263
- export function readStoredDimensions(dataDir) {
264
- const dbPath = join(dataDir, 'audrey.db');
265
- if (!existsSync(dbPath)) return null;
266
- const db = new Database(dbPath, { readonly: true });
267
- try {
268
- const row = db.prepare("SELECT value FROM audrey_config WHERE key = 'dimensions'").get();
269
- return row ? parseInt(row.value, 10) : null;
270
- } catch (err) {
271
- if (err.message?.includes('no such table')) return null;
272
- throw err;
273
- } finally {
274
- db.close();
275
- }
276
- }
277
-
278
- export function closeDatabase(db) {
279
- if (db && db.open) {
280
- db.close();
281
- }
282
- }
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
+ }