persyst-mcp 2.1.3 → 2.2.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/bin/init.js +7 -0
- package/index.js +41 -0
- package/package.json +2 -2
- package/src/database.js +926 -877
- package/src/extractor-heuristic.js +324 -250
- package/src/git.js +7 -1
- package/src/search.js +561 -456
- package/src/server.js +72 -67
- package/src/tools.js +124 -13
- package/src/watcher.js +306 -0
package/src/database.js
CHANGED
|
@@ -1,877 +1,926 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* database.js — SQLite Database Setup & CRUD Operations
|
|
3
|
-
*
|
|
4
|
-
* This file handles everything database-related:
|
|
5
|
-
* - Opens SQLite connection at ~/.persyst/persyst.db
|
|
6
|
-
* - Loads the sqlite-vec extension for vector search
|
|
7
|
-
* - Creates all tables (memories, FTS5 index, vector index)
|
|
8
|
-
* - Runs schema migrations for production-grade bi-temporal model
|
|
9
|
-
* - Exports simple CRUD functions for other modules to use
|
|
10
|
-
*
|
|
11
|
-
* IMPORTANT: better-sqlite3 is SYNCHRONOUS. No async/await here.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import Database from 'better-sqlite3';
|
|
15
|
-
import * as sqliteVec from 'sqlite-vec';
|
|
16
|
-
import { join } from 'path';
|
|
17
|
-
import { homedir } from 'os';
|
|
18
|
-
import { mkdirSync } from 'fs';
|
|
19
|
-
|
|
20
|
-
// ============================================================
|
|
21
|
-
// DATABASE LOCATION
|
|
22
|
-
// Store in ~/.persyst/ per default to persist across sessions
|
|
23
|
-
// ============================================================
|
|
24
|
-
|
|
25
|
-
const DB_DIR = join(homedir(), '.persyst');
|
|
26
|
-
mkdirSync(DB_DIR, { recursive: true });
|
|
27
|
-
const DB_PATH = process.env.NODE_ENV === 'test' ? ':memory:' : join(DB_DIR, 'persyst.db');
|
|
28
|
-
|
|
29
|
-
// ============================================================
|
|
30
|
-
// INITIALIZE CONNECTION
|
|
31
|
-
// ============================================================
|
|
32
|
-
|
|
33
|
-
const db = new Database(DB_PATH);
|
|
34
|
-
db.pragma('journal_mode = WAL'); // Better performance for concurrent reads
|
|
35
|
-
db.pragma('foreign_keys = ON'); // Enforce referential integrity
|
|
36
|
-
db.pragma('mmap_size = 268435456'); // 256MB memory-mapped I/O for faster reads
|
|
37
|
-
|
|
38
|
-
// Load sqlite-vec BEFORE creating any vec0 tables
|
|
39
|
-
sqliteVec.load(db);
|
|
40
|
-
|
|
41
|
-
console.error(`[persyst] Database: ${DB_PATH}`);
|
|
42
|
-
|
|
43
|
-
// ============================================================
|
|
44
|
-
// CREATE TABLES & SCHEMA MIGRATIONS
|
|
45
|
-
// ============================================================
|
|
46
|
-
|
|
47
|
-
// --- Main memories table ---
|
|
48
|
-
db.exec(`
|
|
49
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
50
|
-
id INTEGER PRIMARY KEY,
|
|
51
|
-
content TEXT NOT NULL,
|
|
52
|
-
importance_score REAL DEFAULT 1.0,
|
|
53
|
-
created_at INTEGER DEFAULT (unixepoch()),
|
|
54
|
-
last_accessed INTEGER DEFAULT (unixepoch()),
|
|
55
|
-
access_count INTEGER DEFAULT 0,
|
|
56
|
-
valid_from INTEGER DEFAULT (unixepoch()),
|
|
57
|
-
valid_until INTEGER DEFAULT NULL,
|
|
58
|
-
assertion_time INTEGER DEFAULT (unixepoch())
|
|
59
|
-
)
|
|
60
|
-
`);
|
|
61
|
-
|
|
62
|
-
// --- Migrations for bi-temporal validity on existing tables ---
|
|
63
|
-
try {
|
|
64
|
-
db.exec('ALTER TABLE memories ADD COLUMN valid_from INTEGER DEFAULT (unixepoch())');
|
|
65
|
-
} catch (e) { /* Column already exists */ }
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
db.exec('ALTER TABLE memories ADD COLUMN valid_until INTEGER DEFAULT NULL');
|
|
69
|
-
} catch (e) { /* Column already exists */ }
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
db.exec('ALTER TABLE memories ADD COLUMN assertion_time INTEGER DEFAULT (unixepoch())');
|
|
73
|
-
} catch (e) { /* Column already exists */ }
|
|
74
|
-
|
|
75
|
-
// --- Migration: add namespace column for per-agent isolation ---
|
|
76
|
-
try {
|
|
77
|
-
db.exec("ALTER TABLE memories ADD COLUMN namespace TEXT DEFAULT 'shared'");
|
|
78
|
-
} catch (e) { /* Column already exists */ }
|
|
79
|
-
|
|
80
|
-
// --- Index on namespace for fast filtered queries ---
|
|
81
|
-
try {
|
|
82
|
-
db.exec('CREATE INDEX IF NOT EXISTS idx_memories_namespace ON memories (namespace)');
|
|
83
|
-
} catch (e) { /* Index already exists */ }
|
|
84
|
-
|
|
85
|
-
// --- Contradictions table ---
|
|
86
|
-
db.exec(`
|
|
87
|
-
CREATE TABLE IF NOT EXISTS contradictions (
|
|
88
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
89
|
-
old_memory_id INTEGER NOT NULL,
|
|
90
|
-
new_memory_id INTEGER NOT NULL,
|
|
91
|
-
resolved_at INTEGER DEFAULT (unixepoch()),
|
|
92
|
-
resolution_reason TEXT
|
|
93
|
-
)
|
|
94
|
-
`);
|
|
95
|
-
|
|
96
|
-
// --- Provenance table ---
|
|
97
|
-
db.exec(`
|
|
98
|
-
CREATE TABLE IF NOT EXISTS provenance (
|
|
99
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
100
|
-
memory_id INTEGER NOT NULL,
|
|
101
|
-
source_type TEXT NOT NULL, -- agent | git | manual | api
|
|
102
|
-
source_id TEXT, -- agent name or git hash
|
|
103
|
-
created_at INTEGER DEFAULT (unixepoch()),
|
|
104
|
-
confidence REAL NOT NULL
|
|
105
|
-
)
|
|
106
|
-
`);
|
|
107
|
-
|
|
108
|
-
// --- Agent Stats table ---
|
|
109
|
-
db.exec(`
|
|
110
|
-
CREATE TABLE IF NOT EXISTS agent_stats (
|
|
111
|
-
agent_id TEXT PRIMARY KEY,
|
|
112
|
-
memories_created INTEGER DEFAULT 0,
|
|
113
|
-
memories_confirmed INTEGER DEFAULT 0,
|
|
114
|
-
memories_contradicted INTEGER DEFAULT 0,
|
|
115
|
-
reputation_score REAL DEFAULT 1.0,
|
|
116
|
-
last_active INTEGER DEFAULT (unixepoch())
|
|
117
|
-
)
|
|
118
|
-
`);
|
|
119
|
-
|
|
120
|
-
// --- Migration: add domain column to agent_stats ---
|
|
121
|
-
try {
|
|
122
|
-
db.exec('ALTER TABLE agent_stats ADD COLUMN domain TEXT DEFAULT "general"');
|
|
123
|
-
} catch (e) { /* Column already exists */ }
|
|
124
|
-
|
|
125
|
-
// --- Attestations table ---
|
|
126
|
-
db.exec(`
|
|
127
|
-
CREATE TABLE IF NOT EXISTS attestations (
|
|
128
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
129
|
-
attestation_id TEXT NOT NULL UNIQUE,
|
|
130
|
-
query TEXT NOT NULL,
|
|
131
|
-
timestamp TEXT NOT NULL,
|
|
132
|
-
memories_retrieved TEXT NOT NULL,
|
|
133
|
-
agent_id TEXT,
|
|
134
|
-
session_id TEXT,
|
|
135
|
-
signature TEXT NOT NULL,
|
|
136
|
-
previous_hash TEXT,
|
|
137
|
-
hash TEXT NOT NULL
|
|
138
|
-
)
|
|
139
|
-
`);
|
|
140
|
-
|
|
141
|
-
// --- FTS5 full-text search index (keyword search with BM25) ---
|
|
142
|
-
db.exec(`
|
|
143
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
144
|
-
content,
|
|
145
|
-
content='memories',
|
|
146
|
-
content_rowid='id'
|
|
147
|
-
)
|
|
148
|
-
`);
|
|
149
|
-
|
|
150
|
-
// --- FTS5 auto-sync triggers ---
|
|
151
|
-
try {
|
|
152
|
-
db.exec(`
|
|
153
|
-
CREATE TRIGGER memories_fts_insert AFTER INSERT ON memories
|
|
154
|
-
BEGIN
|
|
155
|
-
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
156
|
-
END
|
|
157
|
-
`);
|
|
158
|
-
} catch (e) { /* trigger already exists */ }
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
db.exec(`
|
|
162
|
-
CREATE TRIGGER memories_fts_delete AFTER DELETE ON memories
|
|
163
|
-
BEGIN
|
|
164
|
-
INSERT INTO memories_fts(memories_fts, rowid, content)
|
|
165
|
-
VALUES ('delete', old.id, old.content);
|
|
166
|
-
END
|
|
167
|
-
`);
|
|
168
|
-
} catch (e) { /* trigger already exists */ }
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
db.exec(`
|
|
172
|
-
CREATE TRIGGER memories_fts_update AFTER UPDATE OF content ON memories
|
|
173
|
-
BEGIN
|
|
174
|
-
INSERT INTO memories_fts(memories_fts, rowid, content)
|
|
175
|
-
VALUES ('delete', old.id, old.content);
|
|
176
|
-
INSERT INTO memories_fts(rowid, content)
|
|
177
|
-
VALUES (new.id, new.content);
|
|
178
|
-
END
|
|
179
|
-
`);
|
|
180
|
-
} catch (e) { /* trigger already exists */ }
|
|
181
|
-
|
|
182
|
-
// --- Vector table for semantic search (384-dim embeddings) ---
|
|
183
|
-
db.exec(`
|
|
184
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_vec USING vec0(
|
|
185
|
-
embedding float[384]
|
|
186
|
-
)
|
|
187
|
-
`);
|
|
188
|
-
|
|
189
|
-
// --- Knowledge Graph: entities + edges ---
|
|
190
|
-
db.exec(`
|
|
191
|
-
CREATE TABLE IF NOT EXISTS entities (
|
|
192
|
-
id INTEGER PRIMARY KEY,
|
|
193
|
-
name TEXT NOT NULL UNIQUE,
|
|
194
|
-
type TEXT NOT NULL,
|
|
195
|
-
created_at INTEGER DEFAULT (unixepoch())
|
|
196
|
-
)
|
|
197
|
-
`);
|
|
198
|
-
|
|
199
|
-
db.exec(`
|
|
200
|
-
CREATE TABLE IF NOT EXISTS edges (
|
|
201
|
-
id INTEGER PRIMARY KEY,
|
|
202
|
-
source_id INTEGER NOT NULL,
|
|
203
|
-
target_id INTEGER NOT NULL,
|
|
204
|
-
relation TEXT NOT NULL,
|
|
205
|
-
source_type TEXT NOT NULL,
|
|
206
|
-
target_type TEXT NOT NULL,
|
|
207
|
-
created_at INTEGER DEFAULT (unixepoch())
|
|
208
|
-
)
|
|
209
|
-
`);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
),
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
'
|
|
296
|
-
),
|
|
297
|
-
|
|
298
|
-
'
|
|
299
|
-
),
|
|
300
|
-
|
|
301
|
-
// --
|
|
302
|
-
|
|
303
|
-
'
|
|
304
|
-
),
|
|
305
|
-
|
|
306
|
-
'
|
|
307
|
-
),
|
|
308
|
-
|
|
309
|
-
// --
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
),
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
'
|
|
358
|
-
),
|
|
359
|
-
|
|
360
|
-
'
|
|
361
|
-
),
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
),
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
),
|
|
378
|
-
|
|
379
|
-
// --
|
|
380
|
-
|
|
381
|
-
'SELECT id FROM memories WHERE content
|
|
382
|
-
),
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
),
|
|
391
|
-
|
|
392
|
-
// --
|
|
393
|
-
|
|
394
|
-
'SELECT
|
|
395
|
-
),
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
'SELECT
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
/**
|
|
466
|
-
* Get a memory by ID
|
|
467
|
-
* @
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
*
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
* FTS5 index auto-updates via trigger.
|
|
513
|
-
*
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
rows
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
*
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
*
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
*
|
|
694
|
-
* @
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
*
|
|
706
|
-
* @
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
*
|
|
756
|
-
*/
|
|
757
|
-
export function
|
|
758
|
-
stmts.
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
//
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1
|
+
/**
|
|
2
|
+
* database.js — SQLite Database Setup & CRUD Operations
|
|
3
|
+
*
|
|
4
|
+
* This file handles everything database-related:
|
|
5
|
+
* - Opens SQLite connection at ~/.persyst/persyst.db
|
|
6
|
+
* - Loads the sqlite-vec extension for vector search
|
|
7
|
+
* - Creates all tables (memories, FTS5 index, vector index)
|
|
8
|
+
* - Runs schema migrations for production-grade bi-temporal model
|
|
9
|
+
* - Exports simple CRUD functions for other modules to use
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: better-sqlite3 is SYNCHRONOUS. No async/await here.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import Database from 'better-sqlite3';
|
|
15
|
+
import * as sqliteVec from 'sqlite-vec';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import { homedir } from 'os';
|
|
18
|
+
import { mkdirSync } from 'fs';
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// DATABASE LOCATION
|
|
22
|
+
// Store in ~/.persyst/ per default to persist across sessions
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
const DB_DIR = join(homedir(), '.persyst');
|
|
26
|
+
mkdirSync(DB_DIR, { recursive: true });
|
|
27
|
+
const DB_PATH = process.env.NODE_ENV === 'test' ? ':memory:' : join(DB_DIR, 'persyst.db');
|
|
28
|
+
|
|
29
|
+
// ============================================================
|
|
30
|
+
// INITIALIZE CONNECTION
|
|
31
|
+
// ============================================================
|
|
32
|
+
|
|
33
|
+
const db = new Database(DB_PATH);
|
|
34
|
+
db.pragma('journal_mode = WAL'); // Better performance for concurrent reads
|
|
35
|
+
db.pragma('foreign_keys = ON'); // Enforce referential integrity
|
|
36
|
+
db.pragma('mmap_size = 268435456'); // 256MB memory-mapped I/O for faster reads
|
|
37
|
+
|
|
38
|
+
// Load sqlite-vec BEFORE creating any vec0 tables
|
|
39
|
+
sqliteVec.load(db);
|
|
40
|
+
|
|
41
|
+
console.error(`[persyst] Database: ${DB_PATH}`);
|
|
42
|
+
|
|
43
|
+
// ============================================================
|
|
44
|
+
// CREATE TABLES & SCHEMA MIGRATIONS
|
|
45
|
+
// ============================================================
|
|
46
|
+
|
|
47
|
+
// --- Main memories table ---
|
|
48
|
+
db.exec(`
|
|
49
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
50
|
+
id INTEGER PRIMARY KEY,
|
|
51
|
+
content TEXT NOT NULL,
|
|
52
|
+
importance_score REAL DEFAULT 1.0,
|
|
53
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
54
|
+
last_accessed INTEGER DEFAULT (unixepoch()),
|
|
55
|
+
access_count INTEGER DEFAULT 0,
|
|
56
|
+
valid_from INTEGER DEFAULT (unixepoch()),
|
|
57
|
+
valid_until INTEGER DEFAULT NULL,
|
|
58
|
+
assertion_time INTEGER DEFAULT (unixepoch())
|
|
59
|
+
)
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
// --- Migrations for bi-temporal validity on existing tables ---
|
|
63
|
+
try {
|
|
64
|
+
db.exec('ALTER TABLE memories ADD COLUMN valid_from INTEGER DEFAULT (unixepoch())');
|
|
65
|
+
} catch (e) { /* Column already exists */ }
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
db.exec('ALTER TABLE memories ADD COLUMN valid_until INTEGER DEFAULT NULL');
|
|
69
|
+
} catch (e) { /* Column already exists */ }
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
db.exec('ALTER TABLE memories ADD COLUMN assertion_time INTEGER DEFAULT (unixepoch())');
|
|
73
|
+
} catch (e) { /* Column already exists */ }
|
|
74
|
+
|
|
75
|
+
// --- Migration: add namespace column for per-agent isolation ---
|
|
76
|
+
try {
|
|
77
|
+
db.exec("ALTER TABLE memories ADD COLUMN namespace TEXT DEFAULT 'shared'");
|
|
78
|
+
} catch (e) { /* Column already exists */ }
|
|
79
|
+
|
|
80
|
+
// --- Index on namespace for fast filtered queries ---
|
|
81
|
+
try {
|
|
82
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_memories_namespace ON memories (namespace)');
|
|
83
|
+
} catch (e) { /* Index already exists */ }
|
|
84
|
+
|
|
85
|
+
// --- Contradictions table ---
|
|
86
|
+
db.exec(`
|
|
87
|
+
CREATE TABLE IF NOT EXISTS contradictions (
|
|
88
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
89
|
+
old_memory_id INTEGER NOT NULL,
|
|
90
|
+
new_memory_id INTEGER NOT NULL,
|
|
91
|
+
resolved_at INTEGER DEFAULT (unixepoch()),
|
|
92
|
+
resolution_reason TEXT
|
|
93
|
+
)
|
|
94
|
+
`);
|
|
95
|
+
|
|
96
|
+
// --- Provenance table ---
|
|
97
|
+
db.exec(`
|
|
98
|
+
CREATE TABLE IF NOT EXISTS provenance (
|
|
99
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
100
|
+
memory_id INTEGER NOT NULL,
|
|
101
|
+
source_type TEXT NOT NULL, -- agent | git | manual | api
|
|
102
|
+
source_id TEXT, -- agent name or git hash
|
|
103
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
104
|
+
confidence REAL NOT NULL
|
|
105
|
+
)
|
|
106
|
+
`);
|
|
107
|
+
|
|
108
|
+
// --- Agent Stats table ---
|
|
109
|
+
db.exec(`
|
|
110
|
+
CREATE TABLE IF NOT EXISTS agent_stats (
|
|
111
|
+
agent_id TEXT PRIMARY KEY,
|
|
112
|
+
memories_created INTEGER DEFAULT 0,
|
|
113
|
+
memories_confirmed INTEGER DEFAULT 0,
|
|
114
|
+
memories_contradicted INTEGER DEFAULT 0,
|
|
115
|
+
reputation_score REAL DEFAULT 1.0,
|
|
116
|
+
last_active INTEGER DEFAULT (unixepoch())
|
|
117
|
+
)
|
|
118
|
+
`);
|
|
119
|
+
|
|
120
|
+
// --- Migration: add domain column to agent_stats ---
|
|
121
|
+
try {
|
|
122
|
+
db.exec('ALTER TABLE agent_stats ADD COLUMN domain TEXT DEFAULT "general"');
|
|
123
|
+
} catch (e) { /* Column already exists */ }
|
|
124
|
+
|
|
125
|
+
// --- Attestations table ---
|
|
126
|
+
db.exec(`
|
|
127
|
+
CREATE TABLE IF NOT EXISTS attestations (
|
|
128
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
129
|
+
attestation_id TEXT NOT NULL UNIQUE,
|
|
130
|
+
query TEXT NOT NULL,
|
|
131
|
+
timestamp TEXT NOT NULL,
|
|
132
|
+
memories_retrieved TEXT NOT NULL,
|
|
133
|
+
agent_id TEXT,
|
|
134
|
+
session_id TEXT,
|
|
135
|
+
signature TEXT NOT NULL,
|
|
136
|
+
previous_hash TEXT,
|
|
137
|
+
hash TEXT NOT NULL
|
|
138
|
+
)
|
|
139
|
+
`);
|
|
140
|
+
|
|
141
|
+
// --- FTS5 full-text search index (keyword search with BM25) ---
|
|
142
|
+
db.exec(`
|
|
143
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
144
|
+
content,
|
|
145
|
+
content='memories',
|
|
146
|
+
content_rowid='id'
|
|
147
|
+
)
|
|
148
|
+
`);
|
|
149
|
+
|
|
150
|
+
// --- FTS5 auto-sync triggers ---
|
|
151
|
+
try {
|
|
152
|
+
db.exec(`
|
|
153
|
+
CREATE TRIGGER memories_fts_insert AFTER INSERT ON memories
|
|
154
|
+
BEGIN
|
|
155
|
+
INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
|
|
156
|
+
END
|
|
157
|
+
`);
|
|
158
|
+
} catch (e) { /* trigger already exists */ }
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
db.exec(`
|
|
162
|
+
CREATE TRIGGER memories_fts_delete AFTER DELETE ON memories
|
|
163
|
+
BEGIN
|
|
164
|
+
INSERT INTO memories_fts(memories_fts, rowid, content)
|
|
165
|
+
VALUES ('delete', old.id, old.content);
|
|
166
|
+
END
|
|
167
|
+
`);
|
|
168
|
+
} catch (e) { /* trigger already exists */ }
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
db.exec(`
|
|
172
|
+
CREATE TRIGGER memories_fts_update AFTER UPDATE OF content ON memories
|
|
173
|
+
BEGIN
|
|
174
|
+
INSERT INTO memories_fts(memories_fts, rowid, content)
|
|
175
|
+
VALUES ('delete', old.id, old.content);
|
|
176
|
+
INSERT INTO memories_fts(rowid, content)
|
|
177
|
+
VALUES (new.id, new.content);
|
|
178
|
+
END
|
|
179
|
+
`);
|
|
180
|
+
} catch (e) { /* trigger already exists */ }
|
|
181
|
+
|
|
182
|
+
// --- Vector table for semantic search (384-dim embeddings) ---
|
|
183
|
+
db.exec(`
|
|
184
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_vec USING vec0(
|
|
185
|
+
embedding float[384]
|
|
186
|
+
)
|
|
187
|
+
`);
|
|
188
|
+
|
|
189
|
+
// --- Knowledge Graph: entities + edges ---
|
|
190
|
+
db.exec(`
|
|
191
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
192
|
+
id INTEGER PRIMARY KEY,
|
|
193
|
+
name TEXT NOT NULL UNIQUE,
|
|
194
|
+
type TEXT NOT NULL,
|
|
195
|
+
created_at INTEGER DEFAULT (unixepoch())
|
|
196
|
+
)
|
|
197
|
+
`);
|
|
198
|
+
|
|
199
|
+
db.exec(`
|
|
200
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
201
|
+
id INTEGER PRIMARY KEY,
|
|
202
|
+
source_id INTEGER NOT NULL,
|
|
203
|
+
target_id INTEGER NOT NULL,
|
|
204
|
+
relation TEXT NOT NULL,
|
|
205
|
+
source_type TEXT NOT NULL,
|
|
206
|
+
target_type TEXT NOT NULL,
|
|
207
|
+
created_at INTEGER DEFAULT (unixepoch())
|
|
208
|
+
)
|
|
209
|
+
`);
|
|
210
|
+
|
|
211
|
+
db.exec(`
|
|
212
|
+
CREATE TABLE IF NOT EXISTS watched_files (
|
|
213
|
+
file_path TEXT PRIMARY KEY,
|
|
214
|
+
last_position INTEGER NOT NULL,
|
|
215
|
+
updated_at INTEGER DEFAULT (unixepoch())
|
|
216
|
+
)
|
|
217
|
+
`);
|
|
218
|
+
|
|
219
|
+
console.error('[persyst] Schema initialized ✓');
|
|
220
|
+
|
|
221
|
+
// ============================================================
|
|
222
|
+
// PREPARED STATEMENTS
|
|
223
|
+
// Pre-compile SQL for performance. better-sqlite3 is synchronous.
|
|
224
|
+
// ============================================================
|
|
225
|
+
|
|
226
|
+
const stmts = {
|
|
227
|
+
// -- Insert --
|
|
228
|
+
insertMemory: db.prepare(
|
|
229
|
+
'INSERT INTO memories (content, importance_score, namespace) VALUES (?, ?, ?)'
|
|
230
|
+
),
|
|
231
|
+
insertVec: db.prepare(
|
|
232
|
+
'INSERT INTO memories_vec (rowid, embedding) VALUES (?, ?)'
|
|
233
|
+
),
|
|
234
|
+
insertProvenance: db.prepare(
|
|
235
|
+
'INSERT INTO provenance (memory_id, source_type, source_id, confidence) VALUES (?, ?, ?, ?)'
|
|
236
|
+
),
|
|
237
|
+
insertContradiction: db.prepare(
|
|
238
|
+
'INSERT INTO contradictions (old_memory_id, new_memory_id, resolution_reason) VALUES (?, ?, ?)'
|
|
239
|
+
),
|
|
240
|
+
upsertAgent: db.prepare(`
|
|
241
|
+
INSERT INTO agent_stats (agent_id) VALUES (?)
|
|
242
|
+
ON CONFLICT(agent_id) DO UPDATE SET last_active = unixepoch()
|
|
243
|
+
`),
|
|
244
|
+
incrementCreated: db.prepare(
|
|
245
|
+
'UPDATE agent_stats SET memories_created = memories_created + 1 WHERE agent_id = ?'
|
|
246
|
+
),
|
|
247
|
+
incrementConfirmed: db.prepare(
|
|
248
|
+
'UPDATE agent_stats SET memories_confirmed = memories_confirmed + 1 WHERE agent_id = ?'
|
|
249
|
+
),
|
|
250
|
+
incrementContradicted: db.prepare(
|
|
251
|
+
'UPDATE agent_stats SET memories_contradicted = memories_contradicted + 1 WHERE agent_id = ?'
|
|
252
|
+
),
|
|
253
|
+
recalculateReputation: db.prepare(
|
|
254
|
+
'UPDATE agent_stats SET reputation_score = (memories_confirmed + 1.0) / (memories_contradicted + 1.0) WHERE agent_id = ?'
|
|
255
|
+
),
|
|
256
|
+
insertAttestation: db.prepare(`
|
|
257
|
+
INSERT INTO attestations (
|
|
258
|
+
attestation_id, query, timestamp, memories_retrieved,
|
|
259
|
+
agent_id, session_id, signature, previous_hash, hash
|
|
260
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
261
|
+
`),
|
|
262
|
+
|
|
263
|
+
// -- Read --
|
|
264
|
+
getById: db.prepare(
|
|
265
|
+
'SELECT * FROM memories WHERE id = ? AND valid_until IS NULL'
|
|
266
|
+
),
|
|
267
|
+
getByIdNs: db.prepare(
|
|
268
|
+
"SELECT * FROM memories WHERE id = ? AND (namespace = ? OR namespace = 'shared') AND valid_until IS NULL"
|
|
269
|
+
),
|
|
270
|
+
getAnyById: db.prepare(
|
|
271
|
+
'SELECT * FROM memories WHERE id = ?'
|
|
272
|
+
),
|
|
273
|
+
getRecent: db.prepare(
|
|
274
|
+
'SELECT * FROM memories WHERE valid_until IS NULL ORDER BY created_at DESC LIMIT ?'
|
|
275
|
+
),
|
|
276
|
+
getRecentNs: db.prepare(
|
|
277
|
+
"SELECT * FROM memories WHERE (namespace = ? OR namespace = 'shared') AND valid_until IS NULL ORDER BY created_at DESC LIMIT ?"
|
|
278
|
+
),
|
|
279
|
+
getImportant: db.prepare(
|
|
280
|
+
'SELECT * FROM memories WHERE valid_until IS NULL ORDER BY importance_score DESC LIMIT ?'
|
|
281
|
+
),
|
|
282
|
+
getImportantNs: db.prepare(
|
|
283
|
+
"SELECT * FROM memories WHERE (namespace = ? OR namespace = 'shared') AND valid_until IS NULL ORDER BY importance_score DESC LIMIT ?"
|
|
284
|
+
),
|
|
285
|
+
getProvenance: db.prepare(
|
|
286
|
+
'SELECT * FROM provenance WHERE memory_id = ?'
|
|
287
|
+
),
|
|
288
|
+
getAllAgentStats: db.prepare(
|
|
289
|
+
'SELECT * FROM agent_stats ORDER BY reputation_score DESC'
|
|
290
|
+
),
|
|
291
|
+
getAttestation: db.prepare(
|
|
292
|
+
'SELECT * FROM attestations WHERE attestation_id = ?'
|
|
293
|
+
),
|
|
294
|
+
getLastAttestation: db.prepare(
|
|
295
|
+
'SELECT * FROM attestations ORDER BY id DESC LIMIT 1'
|
|
296
|
+
),
|
|
297
|
+
getAttestationsByDate: db.prepare(
|
|
298
|
+
'SELECT * FROM attestations WHERE timestamp >= ? AND timestamp <= ? ORDER BY id ASC'
|
|
299
|
+
),
|
|
300
|
+
|
|
301
|
+
// -- Update --
|
|
302
|
+
updateContent: db.prepare(
|
|
303
|
+
'UPDATE memories SET content = ? WHERE id = ?'
|
|
304
|
+
),
|
|
305
|
+
archiveMemory: db.prepare(
|
|
306
|
+
'UPDATE memories SET valid_until = unixepoch() WHERE id = ?'
|
|
307
|
+
),
|
|
308
|
+
|
|
309
|
+
// -- Delete --
|
|
310
|
+
deleteMemory: db.prepare(
|
|
311
|
+
'DELETE FROM memories WHERE id = ?'
|
|
312
|
+
),
|
|
313
|
+
deleteVec: db.prepare(
|
|
314
|
+
'DELETE FROM memories_vec WHERE rowid = ?'
|
|
315
|
+
),
|
|
316
|
+
|
|
317
|
+
// -- Memory Lifecycle --
|
|
318
|
+
boost: db.prepare(`
|
|
319
|
+
UPDATE memories
|
|
320
|
+
SET access_count = access_count + 1,
|
|
321
|
+
importance_score = MIN(importance_score + 0.1, 2.0),
|
|
322
|
+
last_accessed = unixepoch()
|
|
323
|
+
WHERE id = ?
|
|
324
|
+
`),
|
|
325
|
+
decay: db.prepare(`
|
|
326
|
+
UPDATE memories
|
|
327
|
+
SET importance_score = importance_score * 0.95
|
|
328
|
+
WHERE (? - last_accessed) > 604800
|
|
329
|
+
`),
|
|
330
|
+
|
|
331
|
+
// -- Search --
|
|
332
|
+
searchFts: db.prepare(`
|
|
333
|
+
SELECT rowid AS id, rank
|
|
334
|
+
FROM memories_fts
|
|
335
|
+
WHERE memories_fts MATCH ?
|
|
336
|
+
ORDER BY rank
|
|
337
|
+
LIMIT ?
|
|
338
|
+
`),
|
|
339
|
+
searchVec: db.prepare(`
|
|
340
|
+
SELECT rowid, distance
|
|
341
|
+
FROM memories_vec
|
|
342
|
+
WHERE embedding MATCH ?
|
|
343
|
+
AND k = ?
|
|
344
|
+
`),
|
|
345
|
+
|
|
346
|
+
// -- Entity CRUD --
|
|
347
|
+
insertEntity: db.prepare(
|
|
348
|
+
'INSERT OR IGNORE INTO entities (name, type) VALUES (?, ?)'
|
|
349
|
+
),
|
|
350
|
+
getEntityByName: db.prepare(
|
|
351
|
+
'SELECT * FROM entities WHERE name = ?'
|
|
352
|
+
),
|
|
353
|
+
getEntityById: db.prepare(
|
|
354
|
+
'SELECT * FROM entities WHERE id = ?'
|
|
355
|
+
),
|
|
356
|
+
getAllEntities: db.prepare(
|
|
357
|
+
'SELECT * FROM entities ORDER BY created_at DESC LIMIT ?'
|
|
358
|
+
),
|
|
359
|
+
deleteEntity: db.prepare(
|
|
360
|
+
'DELETE FROM entities WHERE id = ?'
|
|
361
|
+
),
|
|
362
|
+
|
|
363
|
+
// -- Edges --
|
|
364
|
+
insertEdge: db.prepare(
|
|
365
|
+
'INSERT INTO edges (source_id, target_id, relation, source_type, target_type) VALUES (?, ?, ?, ?, ?)'
|
|
366
|
+
),
|
|
367
|
+
getEdgesBySource: db.prepare(
|
|
368
|
+
'SELECT * FROM edges WHERE source_id = ? AND source_type = ?'
|
|
369
|
+
),
|
|
370
|
+
getEdgesByTarget: db.prepare(
|
|
371
|
+
'SELECT * FROM edges WHERE target_id = ? AND target_type = ?'
|
|
372
|
+
),
|
|
373
|
+
deleteEdgesByMemory: db.prepare(
|
|
374
|
+
`DELETE FROM edges WHERE
|
|
375
|
+
(source_id = ? AND source_type = 'memory') OR
|
|
376
|
+
(target_id = ? AND target_type = 'memory')`
|
|
377
|
+
),
|
|
378
|
+
|
|
379
|
+
// -- Dedup --
|
|
380
|
+
findMemoryByContent: db.prepare(
|
|
381
|
+
'SELECT id FROM memories WHERE content = ? AND valid_until IS NULL LIMIT 1'
|
|
382
|
+
),
|
|
383
|
+
findMemoryByContentNs: db.prepare(
|
|
384
|
+
"SELECT id FROM memories WHERE content = ? AND (namespace = ? OR namespace = 'shared') AND valid_until IS NULL LIMIT 1"
|
|
385
|
+
),
|
|
386
|
+
|
|
387
|
+
// -- Hash-prefix lookup for git dedup (Bug 1 fix) --
|
|
388
|
+
findMemoryByHashPrefix: db.prepare(
|
|
389
|
+
'SELECT id FROM memories WHERE content LIKE ? AND valid_until IS NULL LIMIT 1'
|
|
390
|
+
),
|
|
391
|
+
|
|
392
|
+
// -- Active memory count --
|
|
393
|
+
getActiveMemoryCount: db.prepare(
|
|
394
|
+
'SELECT COUNT(*) as count FROM memories WHERE valid_until IS NULL'
|
|
395
|
+
),
|
|
396
|
+
getActiveMemoryCountNs: db.prepare(
|
|
397
|
+
"SELECT COUNT(*) as count FROM memories WHERE (namespace = ? OR namespace = 'shared') AND valid_until IS NULL"
|
|
398
|
+
),
|
|
399
|
+
|
|
400
|
+
// -- Namespace stats --
|
|
401
|
+
getNamespaceStats: db.prepare(
|
|
402
|
+
'SELECT namespace, COUNT(*) as count FROM memories WHERE valid_until IS NULL GROUP BY namespace ORDER BY count DESC'
|
|
403
|
+
),
|
|
404
|
+
|
|
405
|
+
// -- Memory History Chain (Feature 6: prepared statements) --
|
|
406
|
+
getContradictionAncestors: db.prepare(
|
|
407
|
+
'SELECT old_memory_id FROM contradictions WHERE new_memory_id = ?'
|
|
408
|
+
),
|
|
409
|
+
getContradictionDescendants: db.prepare(
|
|
410
|
+
'SELECT new_memory_id FROM contradictions WHERE old_memory_id = ?'
|
|
411
|
+
),
|
|
412
|
+
|
|
413
|
+
// -- Watcher Offsets --
|
|
414
|
+
getWatchPosition: db.prepare(
|
|
415
|
+
'SELECT last_position FROM watched_files WHERE file_path = ?'
|
|
416
|
+
),
|
|
417
|
+
upsertWatchPosition: db.prepare(`
|
|
418
|
+
INSERT INTO watched_files (file_path, last_position)
|
|
419
|
+
VALUES (?, ?)
|
|
420
|
+
ON CONFLICT(file_path) DO UPDATE SET last_position = excluded.last_position, updated_at = unixepoch()
|
|
421
|
+
`)
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// ============================================================
|
|
425
|
+
// CRUD FUNCTIONS
|
|
426
|
+
// Simple, one-purpose functions. No magic.
|
|
427
|
+
// ============================================================
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Insert a new memory into the memories table and log its provenance.
|
|
431
|
+
* @param {string} content - Memory content
|
|
432
|
+
* @param {number} importance - Importance score (0-1)
|
|
433
|
+
* @param {Object} provenanceInfo - Provenance metadata
|
|
434
|
+
* @param {string} namespace - Namespace for agent isolation (default: 'shared')
|
|
435
|
+
* @returns {number} The new memory's ID
|
|
436
|
+
*/
|
|
437
|
+
export function insertMemory(content, importance = 1.0, provenanceInfo = null, namespace = 'shared') {
|
|
438
|
+
const result = stmts.insertMemory.run(content, importance, namespace || 'shared');
|
|
439
|
+
const id = Number(result.lastInsertRowid);
|
|
440
|
+
|
|
441
|
+
// Provenance Info handling
|
|
442
|
+
const source_type = provenanceInfo?.source_type || 'manual';
|
|
443
|
+
const source_id = provenanceInfo?.source_id || null;
|
|
444
|
+
const confidence = provenanceInfo?.confidence !== undefined ? provenanceInfo.confidence : 1.0;
|
|
445
|
+
|
|
446
|
+
stmts.insertProvenance.run(id, source_type, source_id, confidence);
|
|
447
|
+
|
|
448
|
+
// Agent Stats handling
|
|
449
|
+
if (source_type === 'agent' && source_id) {
|
|
450
|
+
incrementAgentStat(source_id, 'created');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return id;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Store an embedding vector for a memory.
|
|
458
|
+
* @param {number} id - Memory ID (used as rowid in vec table)
|
|
459
|
+
* @param {Float32Array} embedding - 384-dim embedding vector
|
|
460
|
+
*/
|
|
461
|
+
export function insertVector(id, embedding) {
|
|
462
|
+
stmts.insertVec.run(BigInt(id), Buffer.from(embedding.buffer));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get a memory by ID. Boosts its importance on access.
|
|
467
|
+
* @param {number} id - Memory ID
|
|
468
|
+
* @param {string|null} namespace - Namespace filter (null = no filter)
|
|
469
|
+
* @returns {object|null} The memory row, or null if not found
|
|
470
|
+
*/
|
|
471
|
+
export function getMemory(id, namespace = null) {
|
|
472
|
+
const memory = namespace
|
|
473
|
+
? stmts.getByIdNs.get(id, namespace)
|
|
474
|
+
: stmts.getById.get(id);
|
|
475
|
+
if (memory) {
|
|
476
|
+
boostMemory(id);
|
|
477
|
+
const prov = getProvenance(id);
|
|
478
|
+
memory.provenance = prov;
|
|
479
|
+
}
|
|
480
|
+
return memory || null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Get a memory by ID WITHOUT boosting or checking bi-temporal validity.
|
|
485
|
+
* @returns {object|null} The memory row, or null if not found
|
|
486
|
+
*/
|
|
487
|
+
export function getAnyMemoryById(id) {
|
|
488
|
+
const memory = stmts.getAnyById.get(id);
|
|
489
|
+
if (memory) {
|
|
490
|
+
memory.provenance = getProvenance(id);
|
|
491
|
+
}
|
|
492
|
+
return memory || null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Get a memory by ID WITHOUT boosting. Used internally for search results.
|
|
497
|
+
* @param {number} id - Memory ID
|
|
498
|
+
* @param {string|null} namespace - Namespace filter (null = no filter)
|
|
499
|
+
* @returns {object|null} The memory row, or null if not found
|
|
500
|
+
*/
|
|
501
|
+
export function getMemoryById(id, namespace = null) {
|
|
502
|
+
const memory = namespace
|
|
503
|
+
? stmts.getByIdNs.get(id, namespace)
|
|
504
|
+
: stmts.getById.get(id);
|
|
505
|
+
if (memory) {
|
|
506
|
+
memory.provenance = getProvenance(id);
|
|
507
|
+
}
|
|
508
|
+
return memory || null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Update a memory's content. FTS5 index auto-updates via trigger.
|
|
513
|
+
* Caller must also update the vector embedding separately.
|
|
514
|
+
* @returns {boolean} true if the memory existed and was updated
|
|
515
|
+
*/
|
|
516
|
+
export function updateMemoryContent(id, content) {
|
|
517
|
+
const result = stmts.updateContent.run(content, id);
|
|
518
|
+
return result.changes > 0;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Delete a vector embedding by memory ID.
|
|
523
|
+
*/
|
|
524
|
+
export function deleteVec(id) {
|
|
525
|
+
try { stmts.deleteVec.run(BigInt(id)); } catch (e) { /* may not exist */ }
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Delete a memory, its vector embedding, and all associated graph edges.
|
|
530
|
+
* FTS5 index auto-updates via trigger.
|
|
531
|
+
* @returns {boolean} true if the memory existed and was deleted
|
|
532
|
+
*/
|
|
533
|
+
export function deleteMemory(id) {
|
|
534
|
+
stmts.deleteEdgesByMemory.run(id, id);
|
|
535
|
+
deleteVec(id); // Remove vector first (no cascades on virtual tables)
|
|
536
|
+
const result = stmts.deleteMemory.run(id);
|
|
537
|
+
return result.changes > 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Get the N most recently created memories.
|
|
542
|
+
* @param {number} limit - Max results
|
|
543
|
+
* @param {string|null} namespace - Namespace filter (null = all)
|
|
544
|
+
*/
|
|
545
|
+
export function getRecentMemories(limit = 10, namespace = null) {
|
|
546
|
+
const rows = namespace
|
|
547
|
+
? stmts.getRecentNs.all(namespace, limit)
|
|
548
|
+
: stmts.getRecent.all(limit);
|
|
549
|
+
rows.forEach(r => {
|
|
550
|
+
r.provenance = getProvenance(r.id);
|
|
551
|
+
});
|
|
552
|
+
return rows;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Get the N most important memories (by importance_score).
|
|
557
|
+
* @param {number} limit - Max results
|
|
558
|
+
* @param {string|null} namespace - Namespace filter (null = all)
|
|
559
|
+
*/
|
|
560
|
+
export function getImportantMemories(limit = 10, namespace = null) {
|
|
561
|
+
const rows = namespace
|
|
562
|
+
? stmts.getImportantNs.all(namespace, limit)
|
|
563
|
+
: stmts.getImportant.all(limit);
|
|
564
|
+
rows.forEach(r => {
|
|
565
|
+
r.provenance = getProvenance(r.id);
|
|
566
|
+
});
|
|
567
|
+
return rows;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ============================================================
|
|
571
|
+
// MEMORY LIFECYCLE
|
|
572
|
+
// ============================================================
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Boost a memory's importance when it's accessed.
|
|
576
|
+
* Increments access_count, adds 0.1 to importance (max 2.0),
|
|
577
|
+
* and updates last_accessed timestamp.
|
|
578
|
+
*/
|
|
579
|
+
export function boostMemory(id) {
|
|
580
|
+
stmts.boost.run(id);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Apply temporal decay to old memories.
|
|
585
|
+
* Reduces importance by 5% for memories not accessed in 7+ days.
|
|
586
|
+
* Called automatically every hour by the server.
|
|
587
|
+
*/
|
|
588
|
+
export function applyTemporalDecay() {
|
|
589
|
+
const now = Math.floor(Date.now() / 1000);
|
|
590
|
+
const result = stmts.decay.run(now);
|
|
591
|
+
if (result.changes > 0) {
|
|
592
|
+
console.error(`[persyst] Decay applied to ${result.changes} memories`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ============================================================
|
|
597
|
+
// SEARCH HELPERS (used by search.js)
|
|
598
|
+
// ============================================================
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Keyword search using FTS5 with BM25 ranking.
|
|
602
|
+
* @returns {Array<{id: number, rank: number}>}
|
|
603
|
+
*/
|
|
604
|
+
export function searchKeyword(query, limit = 10) {
|
|
605
|
+
try {
|
|
606
|
+
return stmts.searchFts.all(query, limit);
|
|
607
|
+
} catch (e) {
|
|
608
|
+
// FTS5 can throw on special characters in query
|
|
609
|
+
return [];
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Vector similarity search using sqlite-vec KNN.
|
|
615
|
+
* @param {Float32Array} embedding - Query vector (384-dim)
|
|
616
|
+
* @returns {Array<{rowid: number, distance: number}>}
|
|
617
|
+
*/
|
|
618
|
+
export function searchVector(embedding, limit = 10) {
|
|
619
|
+
return stmts.searchVec.all(Buffer.from(embedding.buffer), limit);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ============================================================
|
|
623
|
+
// ENTITY FUNCTIONS (Knowledge Graph)
|
|
624
|
+
// ============================================================
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Create a named entity (person, tech, project, concept, file).
|
|
628
|
+
* Silently skips if entity with that name already exists.
|
|
629
|
+
* @returns {number|null} The entity ID, or null if already existed
|
|
630
|
+
*/
|
|
631
|
+
export function insertEntity(name, type) {
|
|
632
|
+
const result = stmts.insertEntity.run(name, type);
|
|
633
|
+
if (result.changes === 0) {
|
|
634
|
+
// Already exists — return existing ID
|
|
635
|
+
const existing = stmts.getEntityByName.get(name);
|
|
636
|
+
return existing ? existing.id : null;
|
|
637
|
+
}
|
|
638
|
+
return Number(result.lastInsertRowid);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Get an entity by its name.
|
|
643
|
+
*/
|
|
644
|
+
export function getEntityByName(name) {
|
|
645
|
+
return stmts.getEntityByName.get(name) || null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Get an entity by its ID.
|
|
650
|
+
*/
|
|
651
|
+
export function getEntityById(id) {
|
|
652
|
+
return stmts.getEntityById.get(id) || null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Get all entities, most recent first.
|
|
657
|
+
*/
|
|
658
|
+
export function getAllEntities(limit = 50) {
|
|
659
|
+
return stmts.getAllEntities.all(limit);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Delete an entity and its edges.
|
|
664
|
+
*/
|
|
665
|
+
export function deleteEntity(id) {
|
|
666
|
+
stmts.deleteEntity.run(id);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Create an edge connecting two nodes (entity↔entity or entity↔memory).
|
|
671
|
+
*/
|
|
672
|
+
export function insertEdge(sourceId, targetId, relation, sourceType, targetType) {
|
|
673
|
+
stmts.insertEdge.run(sourceId, targetId, relation, sourceType, targetType);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Get all memories linked to an entity.
|
|
678
|
+
*/
|
|
679
|
+
export function getMemoriesByEntity(entityId) {
|
|
680
|
+
const edges = db.prepare(`
|
|
681
|
+
SELECT * FROM edges
|
|
682
|
+
WHERE (source_id = ? AND source_type = 'entity' AND target_type = 'memory')
|
|
683
|
+
OR (target_id = ? AND target_type = 'entity' AND source_type = 'memory')
|
|
684
|
+
`).all(entityId, entityId);
|
|
685
|
+
const memoryIds = edges.map(e => e.source_type === 'memory' ? e.source_id : e.target_id);
|
|
686
|
+
return memoryIds.map(id => stmts.getById.get(id)).filter(Boolean);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Check if a memory with exact content already exists.
|
|
691
|
+
* Used for deduplication.
|
|
692
|
+
* @param {string} content - Exact content to match
|
|
693
|
+
* @param {string|null} namespace - Namespace filter (null = global dedup)
|
|
694
|
+
* @returns {boolean}
|
|
695
|
+
*/
|
|
696
|
+
export function memoryExists(content, namespace = null) {
|
|
697
|
+
if (namespace) {
|
|
698
|
+
return stmts.findMemoryByContentNs.get(content, namespace) !== undefined;
|
|
699
|
+
}
|
|
700
|
+
return stmts.findMemoryByContent.get(content) !== undefined;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Check if a memory exists by hash prefix pattern (LIKE query).
|
|
705
|
+
* Used for git commit deduplication where we match `[hashPrefix]%`.
|
|
706
|
+
* @param {string} pattern - SQL LIKE pattern to match (e.g. '[abc1234]%')
|
|
707
|
+
* @returns {boolean}
|
|
708
|
+
*/
|
|
709
|
+
export function memoryExistsByHashPrefix(pattern) {
|
|
710
|
+
return stmts.findMemoryByHashPrefix.get(pattern) !== undefined;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Get count of active (non-archived) memories.
|
|
715
|
+
* @param {string|null} namespace - Namespace filter (null = all)
|
|
716
|
+
* @returns {number}
|
|
717
|
+
*/
|
|
718
|
+
export function getActiveMemoryCount(namespace = null) {
|
|
719
|
+
if (namespace) {
|
|
720
|
+
return stmts.getActiveMemoryCountNs.get(namespace).count;
|
|
721
|
+
}
|
|
722
|
+
return stmts.getActiveMemoryCount.get().count;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Get namespace breakdown stats.
|
|
727
|
+
* @returns {Array<{namespace: string, count: number}>}
|
|
728
|
+
*/
|
|
729
|
+
export function getNamespaceStats() {
|
|
730
|
+
return stmts.getNamespaceStats.all();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ============================================================
|
|
734
|
+
// DEDUPLICATION BY EXACT CONTENT
|
|
735
|
+
// ============================================================
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Find memory by exact content.
|
|
739
|
+
* @param {string} content
|
|
740
|
+
* @param {string|null} namespace - Namespace filter (null = global)
|
|
741
|
+
* @returns {object|null} The memory row, or null if not found
|
|
742
|
+
*/
|
|
743
|
+
export function getMemoryByContent(content, namespace = null) {
|
|
744
|
+
const row = namespace
|
|
745
|
+
? stmts.findMemoryByContentNs.get(content, namespace)
|
|
746
|
+
: stmts.findMemoryByContent.get(content);
|
|
747
|
+
return row ? getMemoryById(row.id) : null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ============================================================
|
|
751
|
+
// TEMPORAL CONTRADICTIONS & AGENT STATS & ATTESTATIONS CRUD
|
|
752
|
+
// ============================================================
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Archive a memory and log the contradiction.
|
|
756
|
+
*/
|
|
757
|
+
export function logContradiction(oldMemoryId, newMemoryId, reason = '') {
|
|
758
|
+
stmts.archiveMemory.run(oldMemoryId);
|
|
759
|
+
stmts.insertContradiction.run(oldMemoryId, newMemoryId, reason);
|
|
760
|
+
|
|
761
|
+
// Retrieve provenance of both versions for game-theoretic reputation calculation
|
|
762
|
+
const oldProvenance = getProvenance(oldMemoryId);
|
|
763
|
+
const newProvenance = getProvenance(newMemoryId);
|
|
764
|
+
|
|
765
|
+
if (oldProvenance && oldProvenance.source_type === 'agent' && oldProvenance.source_id) {
|
|
766
|
+
const isSelfCorrection = newProvenance &&
|
|
767
|
+
newProvenance.source_type === 'agent' &&
|
|
768
|
+
newProvenance.source_id === oldProvenance.source_id;
|
|
769
|
+
if (!isSelfCorrection) {
|
|
770
|
+
// Different agent/manual source contradicts the old memory
|
|
771
|
+
incrementAgentStat(oldProvenance.source_id, 'contradicted');
|
|
772
|
+
|
|
773
|
+
// Boost reputation of the confirmer/contradictor if it's an agent
|
|
774
|
+
if (newProvenance && newProvenance.source_type === 'agent' && newProvenance.source_id !== oldProvenance.source_id) {
|
|
775
|
+
incrementAgentStat(newProvenance.source_id, 'confirmed');
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Get provenance for a memory.
|
|
783
|
+
*/
|
|
784
|
+
export function getProvenance(memoryId) {
|
|
785
|
+
return stmts.getProvenance.get(memoryId) || null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Update agent reputation counters.
|
|
790
|
+
*/
|
|
791
|
+
export function incrementAgentStat(agentId, action) {
|
|
792
|
+
stmts.upsertAgent.run(agentId);
|
|
793
|
+
if (action === 'created') {
|
|
794
|
+
stmts.incrementCreated.run(agentId);
|
|
795
|
+
} else if (action === 'confirmed') {
|
|
796
|
+
stmts.incrementConfirmed.run(agentId);
|
|
797
|
+
} else if (action === 'contradicted') {
|
|
798
|
+
stmts.incrementContradicted.run(agentId);
|
|
799
|
+
}
|
|
800
|
+
stmts.recalculateReputation.run(agentId);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Get all agent stats.
|
|
805
|
+
*/
|
|
806
|
+
export function getAllAgentStats() {
|
|
807
|
+
return stmts.getAllAgentStats.all();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Upsert agent signature / record attestation in database.
|
|
812
|
+
*/
|
|
813
|
+
export function insertAttestation(att) {
|
|
814
|
+
stmts.insertAttestation.run(
|
|
815
|
+
att.attestation_id,
|
|
816
|
+
att.query,
|
|
817
|
+
att.timestamp,
|
|
818
|
+
JSON.stringify(att.memories_retrieved),
|
|
819
|
+
att.agent_id || null,
|
|
820
|
+
att.session_id || null,
|
|
821
|
+
att.signature,
|
|
822
|
+
att.previous_hash || null,
|
|
823
|
+
att.hash
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Retrieve a specific attestation by ID.
|
|
829
|
+
*/
|
|
830
|
+
export function getAttestationById(attestationId) {
|
|
831
|
+
return stmts.getAttestation.get(attestationId) || null;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Retrieve the last attestation logged for chaining.
|
|
836
|
+
*/
|
|
837
|
+
export function getLastAttestation() {
|
|
838
|
+
return stmts.getLastAttestation.get() || null;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Retrieve attestations within a timestamp range.
|
|
843
|
+
*/
|
|
844
|
+
export function getAttestationsByDateRange(startDate, endDate) {
|
|
845
|
+
return stmts.getAttestationsByDate.all(startDate, endDate);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Traverses contradictions to get historical versions of a memory.
|
|
850
|
+
*/
|
|
851
|
+
export function getMemoryHistoryChain(memoryId) {
|
|
852
|
+
const versions = new Set();
|
|
853
|
+
const queue = [memoryId];
|
|
854
|
+
|
|
855
|
+
while (queue.length > 0) {
|
|
856
|
+
const currentId = queue.shift();
|
|
857
|
+
if (versions.has(currentId)) continue;
|
|
858
|
+
versions.add(currentId);
|
|
859
|
+
|
|
860
|
+
// Find ancestors (replaced by current) — using prepared statement
|
|
861
|
+
const ancestors = stmts.getContradictionAncestors.all(currentId);
|
|
862
|
+
ancestors.forEach(a => {
|
|
863
|
+
if (!versions.has(a.old_memory_id)) queue.push(a.old_memory_id);
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// Find descendants (replaces current) — using prepared statement
|
|
867
|
+
const descendants = stmts.getContradictionDescendants.all(currentId);
|
|
868
|
+
descendants.forEach(d => {
|
|
869
|
+
if (!versions.has(d.new_memory_id)) queue.push(d.new_memory_id);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const ids = Array.from(versions);
|
|
874
|
+
if (ids.length === 0) return [];
|
|
875
|
+
|
|
876
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
877
|
+
const rows = db.prepare(`
|
|
878
|
+
SELECT m.*, p.source_type, p.source_id, p.confidence
|
|
879
|
+
FROM memories m
|
|
880
|
+
LEFT JOIN provenance p ON m.id = p.memory_id
|
|
881
|
+
WHERE m.id IN (${placeholders})
|
|
882
|
+
ORDER BY m.created_at ASC
|
|
883
|
+
`).all(...ids);
|
|
884
|
+
|
|
885
|
+
return rows;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Search all memories FTS (including archived memories).
|
|
890
|
+
*/
|
|
891
|
+
export function searchAllMemoriesFts(queryText, limit = 10) {
|
|
892
|
+
try {
|
|
893
|
+
return stmts.searchFts.all(queryText, limit);
|
|
894
|
+
} catch (e) {
|
|
895
|
+
return [];
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Retrieve the last read position of a watched file.
|
|
901
|
+
*/
|
|
902
|
+
export function getWatchPosition(filePath) {
|
|
903
|
+
const row = stmts.getWatchPosition.get(filePath);
|
|
904
|
+
return row ? row.last_position : 0;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Upsert the last read position of a watched file.
|
|
909
|
+
*/
|
|
910
|
+
export function upsertWatchPosition(filePath, position) {
|
|
911
|
+
stmts.upsertWatchPosition.run(filePath, position);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// ============================================================
|
|
915
|
+
// CLEANUP
|
|
916
|
+
// ============================================================
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Close the database connection. Call on shutdown.
|
|
920
|
+
*/
|
|
921
|
+
export function closeDatabase() {
|
|
922
|
+
db.close();
|
|
923
|
+
console.error('[persyst] Database closed');
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export default db;
|