persyst-mcp 1.0.1 → 2.0.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/LICENSE +21 -0
- package/README.md +85 -62
- package/index.js +4 -1
- package/package.json +16 -2
- package/src/attestation.js +206 -0
- package/src/cache.js +122 -0
- package/src/database.js +369 -33
- package/src/git.js +87 -20
- package/src/search.js +375 -49
- package/src/server.js +19 -4
- package/src/tools.js +502 -98
package/src/database.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - Opens SQLite connection at ~/.persyst/persyst.db
|
|
6
6
|
* - Loads the sqlite-vec extension for vector search
|
|
7
7
|
* - Creates all tables (memories, FTS5 index, vector index)
|
|
8
|
+
* - Runs schema migrations for production-grade bi-temporal model
|
|
8
9
|
* - Exports simple CRUD functions for other modules to use
|
|
9
10
|
*
|
|
10
11
|
* IMPORTANT: better-sqlite3 is SYNCHRONOUS. No async/await here.
|
|
@@ -18,7 +19,7 @@ import { mkdirSync } from 'fs';
|
|
|
18
19
|
|
|
19
20
|
// ============================================================
|
|
20
21
|
// DATABASE LOCATION
|
|
21
|
-
// Store in ~/.persyst/
|
|
22
|
+
// Store in ~/.persyst/ per default to persist across sessions
|
|
22
23
|
// ============================================================
|
|
23
24
|
|
|
24
25
|
const DB_DIR = join(homedir(), '.persyst');
|
|
@@ -32,6 +33,7 @@ const DB_PATH = process.env.NODE_ENV === 'test' ? ':memory:' : join(DB_DIR, 'per
|
|
|
32
33
|
const db = new Database(DB_PATH);
|
|
33
34
|
db.pragma('journal_mode = WAL'); // Better performance for concurrent reads
|
|
34
35
|
db.pragma('foreign_keys = ON'); // Enforce referential integrity
|
|
36
|
+
db.pragma('mmap_size = 268435456'); // 256MB memory-mapped I/O for faster reads
|
|
35
37
|
|
|
36
38
|
// Load sqlite-vec BEFORE creating any vec0 tables
|
|
37
39
|
sqliteVec.load(db);
|
|
@@ -39,7 +41,7 @@ sqliteVec.load(db);
|
|
|
39
41
|
console.error(`[persyst] Database: ${DB_PATH}`);
|
|
40
42
|
|
|
41
43
|
// ============================================================
|
|
42
|
-
// CREATE TABLES
|
|
44
|
+
// CREATE TABLES & SCHEMA MIGRATIONS
|
|
43
45
|
// ============================================================
|
|
44
46
|
|
|
45
47
|
// --- Main memories table ---
|
|
@@ -50,7 +52,79 @@ db.exec(`
|
|
|
50
52
|
importance_score REAL DEFAULT 1.0,
|
|
51
53
|
created_at INTEGER DEFAULT (unixepoch()),
|
|
52
54
|
last_accessed INTEGER DEFAULT (unixepoch()),
|
|
53
|
-
access_count INTEGER DEFAULT 0
|
|
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
|
+
// --- Contradictions table ---
|
|
76
|
+
db.exec(`
|
|
77
|
+
CREATE TABLE IF NOT EXISTS contradictions (
|
|
78
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
79
|
+
old_memory_id INTEGER NOT NULL,
|
|
80
|
+
new_memory_id INTEGER NOT NULL,
|
|
81
|
+
resolved_at INTEGER DEFAULT (unixepoch()),
|
|
82
|
+
resolution_reason TEXT
|
|
83
|
+
)
|
|
84
|
+
`);
|
|
85
|
+
|
|
86
|
+
// --- Provenance table ---
|
|
87
|
+
db.exec(`
|
|
88
|
+
CREATE TABLE IF NOT EXISTS provenance (
|
|
89
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
90
|
+
memory_id INTEGER NOT NULL,
|
|
91
|
+
source_type TEXT NOT NULL, -- agent | git | manual | api
|
|
92
|
+
source_id TEXT, -- agent name or git hash
|
|
93
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
94
|
+
confidence REAL NOT NULL
|
|
95
|
+
)
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
// --- Agent Stats table ---
|
|
99
|
+
db.exec(`
|
|
100
|
+
CREATE TABLE IF NOT EXISTS agent_stats (
|
|
101
|
+
agent_id TEXT PRIMARY KEY,
|
|
102
|
+
memories_created INTEGER DEFAULT 0,
|
|
103
|
+
memories_confirmed INTEGER DEFAULT 0,
|
|
104
|
+
memories_contradicted INTEGER DEFAULT 0,
|
|
105
|
+
reputation_score REAL DEFAULT 1.0,
|
|
106
|
+
last_active INTEGER DEFAULT (unixepoch())
|
|
107
|
+
)
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
// --- Migration: add domain column to agent_stats ---
|
|
111
|
+
try {
|
|
112
|
+
db.exec('ALTER TABLE agent_stats ADD COLUMN domain TEXT DEFAULT "general"');
|
|
113
|
+
} catch (e) { /* Column already exists */ }
|
|
114
|
+
|
|
115
|
+
// --- Attestations table ---
|
|
116
|
+
db.exec(`
|
|
117
|
+
CREATE TABLE IF NOT EXISTS attestations (
|
|
118
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
119
|
+
attestation_id TEXT NOT NULL UNIQUE,
|
|
120
|
+
query TEXT NOT NULL,
|
|
121
|
+
timestamp TEXT NOT NULL,
|
|
122
|
+
memories_retrieved TEXT NOT NULL,
|
|
123
|
+
agent_id TEXT,
|
|
124
|
+
session_id TEXT,
|
|
125
|
+
signature TEXT NOT NULL,
|
|
126
|
+
previous_hash TEXT,
|
|
127
|
+
hash TEXT NOT NULL
|
|
54
128
|
)
|
|
55
129
|
`);
|
|
56
130
|
|
|
@@ -64,9 +138,6 @@ db.exec(`
|
|
|
64
138
|
`);
|
|
65
139
|
|
|
66
140
|
// --- FTS5 auto-sync triggers ---
|
|
67
|
-
// These keep the FTS index in sync when memories are added/updated/deleted.
|
|
68
|
-
// Using try/catch because "CREATE TRIGGER IF NOT EXISTS" isn't supported.
|
|
69
|
-
|
|
70
141
|
try {
|
|
71
142
|
db.exec(`
|
|
72
143
|
CREATE TRIGGER memories_fts_insert AFTER INSERT ON memories
|
|
@@ -106,7 +177,6 @@ db.exec(`
|
|
|
106
177
|
`);
|
|
107
178
|
|
|
108
179
|
// --- Knowledge Graph: entities + edges ---
|
|
109
|
-
// Entities are the "nouns" — people, files, tech, concepts
|
|
110
180
|
db.exec(`
|
|
111
181
|
CREATE TABLE IF NOT EXISTS entities (
|
|
112
182
|
id INTEGER PRIMARY KEY,
|
|
@@ -116,7 +186,6 @@ db.exec(`
|
|
|
116
186
|
)
|
|
117
187
|
`);
|
|
118
188
|
|
|
119
|
-
// Edges connect entities to memories (or entities to entities)
|
|
120
189
|
db.exec(`
|
|
121
190
|
CREATE TABLE IF NOT EXISTS edges (
|
|
122
191
|
id INTEGER PRIMARY KEY,
|
|
@@ -144,22 +213,71 @@ const stmts = {
|
|
|
144
213
|
insertVec: db.prepare(
|
|
145
214
|
'INSERT INTO memories_vec (rowid, embedding) VALUES (?, ?)'
|
|
146
215
|
),
|
|
216
|
+
insertProvenance: db.prepare(
|
|
217
|
+
'INSERT INTO provenance (memory_id, source_type, source_id, confidence) VALUES (?, ?, ?, ?)'
|
|
218
|
+
),
|
|
219
|
+
insertContradiction: db.prepare(
|
|
220
|
+
'INSERT INTO contradictions (old_memory_id, new_memory_id, resolution_reason) VALUES (?, ?, ?)'
|
|
221
|
+
),
|
|
222
|
+
upsertAgent: db.prepare(`
|
|
223
|
+
INSERT INTO agent_stats (agent_id) VALUES (?)
|
|
224
|
+
ON CONFLICT(agent_id) DO UPDATE SET last_active = unixepoch()
|
|
225
|
+
`),
|
|
226
|
+
incrementCreated: db.prepare(
|
|
227
|
+
'UPDATE agent_stats SET memories_created = memories_created + 1 WHERE agent_id = ?'
|
|
228
|
+
),
|
|
229
|
+
incrementConfirmed: db.prepare(
|
|
230
|
+
'UPDATE agent_stats SET memories_confirmed = memories_confirmed + 1 WHERE agent_id = ?'
|
|
231
|
+
),
|
|
232
|
+
incrementContradicted: db.prepare(
|
|
233
|
+
'UPDATE agent_stats SET memories_contradicted = memories_contradicted + 1 WHERE agent_id = ?'
|
|
234
|
+
),
|
|
235
|
+
recalculateReputation: db.prepare(
|
|
236
|
+
'UPDATE agent_stats SET reputation_score = (memories_confirmed + 1.0) / (memories_contradicted + 1.0) WHERE agent_id = ?'
|
|
237
|
+
),
|
|
238
|
+
insertAttestation: db.prepare(`
|
|
239
|
+
INSERT INTO attestations (
|
|
240
|
+
attestation_id, query, timestamp, memories_retrieved,
|
|
241
|
+
agent_id, session_id, signature, previous_hash, hash
|
|
242
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
243
|
+
`),
|
|
147
244
|
|
|
148
245
|
// -- Read --
|
|
149
246
|
getById: db.prepare(
|
|
247
|
+
'SELECT * FROM memories WHERE id = ? AND valid_until IS NULL'
|
|
248
|
+
),
|
|
249
|
+
getAnyById: db.prepare(
|
|
150
250
|
'SELECT * FROM memories WHERE id = ?'
|
|
151
251
|
),
|
|
152
252
|
getRecent: db.prepare(
|
|
153
|
-
'SELECT * FROM memories ORDER BY created_at DESC LIMIT ?'
|
|
253
|
+
'SELECT * FROM memories WHERE valid_until IS NULL ORDER BY created_at DESC LIMIT ?'
|
|
154
254
|
),
|
|
155
255
|
getImportant: db.prepare(
|
|
156
|
-
'SELECT * FROM memories ORDER BY importance_score DESC LIMIT ?'
|
|
256
|
+
'SELECT * FROM memories WHERE valid_until IS NULL ORDER BY importance_score DESC LIMIT ?'
|
|
257
|
+
),
|
|
258
|
+
getProvenance: db.prepare(
|
|
259
|
+
'SELECT * FROM provenance WHERE memory_id = ?'
|
|
260
|
+
),
|
|
261
|
+
getAllAgentStats: db.prepare(
|
|
262
|
+
'SELECT * FROM agent_stats ORDER BY reputation_score DESC'
|
|
263
|
+
),
|
|
264
|
+
getAttestation: db.prepare(
|
|
265
|
+
'SELECT * FROM attestations WHERE attestation_id = ?'
|
|
266
|
+
),
|
|
267
|
+
getLastAttestation: db.prepare(
|
|
268
|
+
'SELECT * FROM attestations ORDER BY id DESC LIMIT 1'
|
|
269
|
+
),
|
|
270
|
+
getAttestationsByDate: db.prepare(
|
|
271
|
+
'SELECT * FROM attestations WHERE timestamp >= ? AND timestamp <= ? ORDER BY id ASC'
|
|
157
272
|
),
|
|
158
273
|
|
|
159
274
|
// -- Update --
|
|
160
275
|
updateContent: db.prepare(
|
|
161
276
|
'UPDATE memories SET content = ? WHERE id = ?'
|
|
162
277
|
),
|
|
278
|
+
archiveMemory: db.prepare(
|
|
279
|
+
'UPDATE memories SET valid_until = unixepoch() WHERE id = ?'
|
|
280
|
+
),
|
|
163
281
|
|
|
164
282
|
// -- Delete --
|
|
165
283
|
deleteMemory: db.prepare(
|
|
@@ -233,7 +351,25 @@ const stmts = {
|
|
|
233
351
|
|
|
234
352
|
// -- Dedup --
|
|
235
353
|
findMemoryByContent: db.prepare(
|
|
236
|
-
'SELECT id FROM memories WHERE content
|
|
354
|
+
'SELECT id FROM memories WHERE content = ? AND valid_until IS NULL LIMIT 1'
|
|
355
|
+
),
|
|
356
|
+
|
|
357
|
+
// -- Hash-prefix lookup for git dedup (Bug 1 fix) --
|
|
358
|
+
findMemoryByHashPrefix: db.prepare(
|
|
359
|
+
'SELECT id FROM memories WHERE content LIKE ? AND valid_until IS NULL LIMIT 1'
|
|
360
|
+
),
|
|
361
|
+
|
|
362
|
+
// -- Active memory count --
|
|
363
|
+
getActiveMemoryCount: db.prepare(
|
|
364
|
+
'SELECT COUNT(*) as count FROM memories WHERE valid_until IS NULL'
|
|
365
|
+
),
|
|
366
|
+
|
|
367
|
+
// -- Memory History Chain (Feature 6: prepared statements) --
|
|
368
|
+
getContradictionAncestors: db.prepare(
|
|
369
|
+
'SELECT old_memory_id FROM contradictions WHERE new_memory_id = ?'
|
|
370
|
+
),
|
|
371
|
+
getContradictionDescendants: db.prepare(
|
|
372
|
+
'SELECT new_memory_id FROM contradictions WHERE old_memory_id = ?'
|
|
237
373
|
)
|
|
238
374
|
};
|
|
239
375
|
|
|
@@ -243,13 +379,26 @@ const stmts = {
|
|
|
243
379
|
// ============================================================
|
|
244
380
|
|
|
245
381
|
/**
|
|
246
|
-
* Insert a new memory into the memories table.
|
|
247
|
-
* FTS5 index is auto-updated via trigger.
|
|
382
|
+
* Insert a new memory into the memories table and log its provenance.
|
|
248
383
|
* @returns {number} The new memory's ID
|
|
249
384
|
*/
|
|
250
|
-
export function insertMemory(content, importance = 1.0) {
|
|
385
|
+
export function insertMemory(content, importance = 1.0, provenanceInfo = null) {
|
|
251
386
|
const result = stmts.insertMemory.run(content, importance);
|
|
252
|
-
|
|
387
|
+
const id = Number(result.lastInsertRowid);
|
|
388
|
+
|
|
389
|
+
// Provenance Info handling
|
|
390
|
+
const source_type = provenanceInfo?.source_type || 'manual';
|
|
391
|
+
const source_id = provenanceInfo?.source_id || null;
|
|
392
|
+
const confidence = provenanceInfo?.confidence !== undefined ? provenanceInfo.confidence : 1.0;
|
|
393
|
+
|
|
394
|
+
stmts.insertProvenance.run(id, source_type, source_id, confidence);
|
|
395
|
+
|
|
396
|
+
// Agent Stats handling
|
|
397
|
+
if (source_type === 'agent' && source_id) {
|
|
398
|
+
incrementAgentStat(source_id, 'created');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return id;
|
|
253
402
|
}
|
|
254
403
|
|
|
255
404
|
/**
|
|
@@ -258,7 +407,6 @@ export function insertMemory(content, importance = 1.0) {
|
|
|
258
407
|
* @param {Float32Array} embedding - 384-dim embedding vector
|
|
259
408
|
*/
|
|
260
409
|
export function insertVector(id, embedding) {
|
|
261
|
-
// better-sqlite3 needs Buffer, sqlite-vec needs BigInt for rowid
|
|
262
410
|
stmts.insertVec.run(BigInt(id), Buffer.from(embedding.buffer));
|
|
263
411
|
}
|
|
264
412
|
|
|
@@ -268,7 +416,24 @@ export function insertVector(id, embedding) {
|
|
|
268
416
|
*/
|
|
269
417
|
export function getMemory(id) {
|
|
270
418
|
const memory = stmts.getById.get(id);
|
|
271
|
-
if (memory)
|
|
419
|
+
if (memory) {
|
|
420
|
+
boostMemory(id);
|
|
421
|
+
// Fetch and link provenance info
|
|
422
|
+
const prov = getProvenance(id);
|
|
423
|
+
memory.provenance = prov;
|
|
424
|
+
}
|
|
425
|
+
return memory || null;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get a memory by ID WITHOUT boosting or checking bi-temporal validity.
|
|
430
|
+
* @returns {object|null} The memory row, or null if not found
|
|
431
|
+
*/
|
|
432
|
+
export function getAnyMemoryById(id) {
|
|
433
|
+
const memory = stmts.getAnyById.get(id);
|
|
434
|
+
if (memory) {
|
|
435
|
+
memory.provenance = getProvenance(id);
|
|
436
|
+
}
|
|
272
437
|
return memory || null;
|
|
273
438
|
}
|
|
274
439
|
|
|
@@ -277,7 +442,11 @@ export function getMemory(id) {
|
|
|
277
442
|
* @returns {object|null} The memory row, or null if not found
|
|
278
443
|
*/
|
|
279
444
|
export function getMemoryById(id) {
|
|
280
|
-
|
|
445
|
+
const memory = stmts.getById.get(id);
|
|
446
|
+
if (memory) {
|
|
447
|
+
memory.provenance = getProvenance(id);
|
|
448
|
+
}
|
|
449
|
+
return memory || null;
|
|
281
450
|
}
|
|
282
451
|
|
|
283
452
|
/**
|
|
@@ -298,11 +467,12 @@ export function deleteVec(id) {
|
|
|
298
467
|
}
|
|
299
468
|
|
|
300
469
|
/**
|
|
301
|
-
* Delete a memory
|
|
470
|
+
* Delete a memory, its vector embedding, and all associated graph edges.
|
|
302
471
|
* FTS5 index auto-updates via trigger.
|
|
303
472
|
* @returns {boolean} true if the memory existed and was deleted
|
|
304
473
|
*/
|
|
305
474
|
export function deleteMemory(id) {
|
|
475
|
+
stmts.deleteEdgesByMemory.run(id, id);
|
|
306
476
|
deleteVec(id); // Remove vector first (no cascades on virtual tables)
|
|
307
477
|
const result = stmts.deleteMemory.run(id);
|
|
308
478
|
return result.changes > 0;
|
|
@@ -312,14 +482,22 @@ export function deleteMemory(id) {
|
|
|
312
482
|
* Get the N most recently created memories.
|
|
313
483
|
*/
|
|
314
484
|
export function getRecentMemories(limit = 10) {
|
|
315
|
-
|
|
485
|
+
const rows = stmts.getRecent.all(limit);
|
|
486
|
+
rows.forEach(r => {
|
|
487
|
+
r.provenance = getProvenance(r.id);
|
|
488
|
+
});
|
|
489
|
+
return rows;
|
|
316
490
|
}
|
|
317
491
|
|
|
318
492
|
/**
|
|
319
493
|
* Get the N most important memories (by importance_score).
|
|
320
494
|
*/
|
|
321
495
|
export function getImportantMemories(limit = 10) {
|
|
322
|
-
|
|
496
|
+
const rows = stmts.getImportant.all(limit);
|
|
497
|
+
rows.forEach(r => {
|
|
498
|
+
r.provenance = getProvenance(r.id);
|
|
499
|
+
});
|
|
500
|
+
return rows;
|
|
323
501
|
}
|
|
324
502
|
|
|
325
503
|
// ============================================================
|
|
@@ -379,7 +557,7 @@ export function searchVector(embedding, limit = 10) {
|
|
|
379
557
|
// ============================================================
|
|
380
558
|
|
|
381
559
|
/**
|
|
382
|
-
* Create a named entity (person, tech,
|
|
560
|
+
* Create a named entity (person, tech, project, concept, file).
|
|
383
561
|
* Silently skips if entity with that name already exists.
|
|
384
562
|
* @returns {number|null} The entity ID, or null if already existed
|
|
385
563
|
*/
|
|
@@ -439,23 +617,181 @@ export function getMemoriesByEntity(entityId) {
|
|
|
439
617
|
}
|
|
440
618
|
|
|
441
619
|
/**
|
|
442
|
-
* Check if a memory with
|
|
443
|
-
* Used for deduplication
|
|
444
|
-
* @param {string}
|
|
620
|
+
* Check if a memory with exact content already exists.
|
|
621
|
+
* Used for deduplication.
|
|
622
|
+
* @param {string} content - Exact content to match
|
|
445
623
|
* @returns {boolean}
|
|
446
624
|
*/
|
|
447
|
-
export function memoryExists(
|
|
448
|
-
return stmts.findMemoryByContent.get(
|
|
625
|
+
export function memoryExists(content) {
|
|
626
|
+
return stmts.findMemoryByContent.get(content) !== undefined;
|
|
449
627
|
}
|
|
450
628
|
|
|
451
629
|
/**
|
|
452
|
-
*
|
|
630
|
+
* Check if a memory exists by hash prefix pattern (LIKE query).
|
|
631
|
+
* Used for git commit deduplication where we match `[hashPrefix]%`.
|
|
632
|
+
* @param {string} pattern - SQL LIKE pattern to match (e.g. '[abc1234]%')
|
|
633
|
+
* @returns {boolean}
|
|
453
634
|
*/
|
|
454
|
-
export function
|
|
455
|
-
stmts.
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
635
|
+
export function memoryExistsByHashPrefix(pattern) {
|
|
636
|
+
return stmts.findMemoryByHashPrefix.get(pattern) !== undefined;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Get count of active (non-archived) memories.
|
|
641
|
+
* @returns {number}
|
|
642
|
+
*/
|
|
643
|
+
export function getActiveMemoryCount() {
|
|
644
|
+
return stmts.getActiveMemoryCount.get().count;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ============================================================
|
|
648
|
+
// DEDUPLICATION BY EXACT CONTENT
|
|
649
|
+
// ============================================================
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Find memory by exact content.
|
|
653
|
+
* @param {string} content
|
|
654
|
+
* @returns {object|null} The memory row, or null if not found
|
|
655
|
+
*/
|
|
656
|
+
export function getMemoryByContent(content) {
|
|
657
|
+
const row = stmts.findMemoryByContent.get(content);
|
|
658
|
+
return row ? getMemoryById(row.id) : null;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ============================================================
|
|
662
|
+
// TEMPORAL CONTRADICTIONS & AGENT STATS & ATTESTATIONS CRUD
|
|
663
|
+
// ============================================================
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Archive a memory and log the contradiction.
|
|
667
|
+
*/
|
|
668
|
+
export function logContradiction(oldMemoryId, newMemoryId, reason = '') {
|
|
669
|
+
stmts.archiveMemory.run(oldMemoryId);
|
|
670
|
+
stmts.insertContradiction.run(oldMemoryId, newMemoryId, reason);
|
|
671
|
+
|
|
672
|
+
// Track that the agent's memory was contradicted
|
|
673
|
+
const oldProvenance = getProvenance(oldMemoryId);
|
|
674
|
+
if (oldProvenance && oldProvenance.source_type === 'agent' && oldProvenance.source_id) {
|
|
675
|
+
incrementAgentStat(oldProvenance.source_id, 'contradicted');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Get provenance for a memory.
|
|
681
|
+
*/
|
|
682
|
+
export function getProvenance(memoryId) {
|
|
683
|
+
return stmts.getProvenance.get(memoryId) || null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Update agent reputation counters.
|
|
688
|
+
*/
|
|
689
|
+
export function incrementAgentStat(agentId, action) {
|
|
690
|
+
stmts.upsertAgent.run(agentId);
|
|
691
|
+
if (action === 'created') {
|
|
692
|
+
stmts.incrementCreated.run(agentId);
|
|
693
|
+
} else if (action === 'confirmed') {
|
|
694
|
+
stmts.incrementConfirmed.run(agentId);
|
|
695
|
+
} else if (action === 'contradicted') {
|
|
696
|
+
stmts.incrementContradicted.run(agentId);
|
|
697
|
+
}
|
|
698
|
+
stmts.recalculateReputation.run(agentId);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Get all agent stats.
|
|
703
|
+
*/
|
|
704
|
+
export function getAllAgentStats() {
|
|
705
|
+
return stmts.getAllAgentStats.all();
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Upsert agent signature / record attestation in database.
|
|
710
|
+
*/
|
|
711
|
+
export function insertAttestation(att) {
|
|
712
|
+
stmts.insertAttestation.run(
|
|
713
|
+
att.attestation_id,
|
|
714
|
+
att.query,
|
|
715
|
+
att.timestamp,
|
|
716
|
+
JSON.stringify(att.memories_retrieved),
|
|
717
|
+
att.agent_id || null,
|
|
718
|
+
att.session_id || null,
|
|
719
|
+
att.signature,
|
|
720
|
+
att.previous_hash || null,
|
|
721
|
+
att.hash
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Retrieve a specific attestation by ID.
|
|
727
|
+
*/
|
|
728
|
+
export function getAttestationById(attestationId) {
|
|
729
|
+
return stmts.getAttestation.get(attestationId) || null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Retrieve the last attestation logged for chaining.
|
|
734
|
+
*/
|
|
735
|
+
export function getLastAttestation() {
|
|
736
|
+
return stmts.getLastAttestation.get() || null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Retrieve attestations within a timestamp range.
|
|
741
|
+
*/
|
|
742
|
+
export function getAttestationsByDateRange(startDate, endDate) {
|
|
743
|
+
return stmts.getAttestationsByDate.all(startDate, endDate);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Traverses contradictions to get historical versions of a memory.
|
|
748
|
+
*/
|
|
749
|
+
export function getMemoryHistoryChain(memoryId) {
|
|
750
|
+
const versions = new Set();
|
|
751
|
+
const queue = [memoryId];
|
|
752
|
+
|
|
753
|
+
while (queue.length > 0) {
|
|
754
|
+
const currentId = queue.shift();
|
|
755
|
+
if (versions.has(currentId)) continue;
|
|
756
|
+
versions.add(currentId);
|
|
757
|
+
|
|
758
|
+
// Find ancestors (replaced by current) — using prepared statement
|
|
759
|
+
const ancestors = stmts.getContradictionAncestors.all(currentId);
|
|
760
|
+
ancestors.forEach(a => {
|
|
761
|
+
if (!versions.has(a.old_memory_id)) queue.push(a.old_memory_id);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// Find descendants (replaces current) — using prepared statement
|
|
765
|
+
const descendants = stmts.getContradictionDescendants.all(currentId);
|
|
766
|
+
descendants.forEach(d => {
|
|
767
|
+
if (!versions.has(d.new_memory_id)) queue.push(d.new_memory_id);
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const ids = Array.from(versions);
|
|
772
|
+
if (ids.length === 0) return [];
|
|
773
|
+
|
|
774
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
775
|
+
const rows = db.prepare(`
|
|
776
|
+
SELECT m.*, p.source_type, p.source_id, p.confidence
|
|
777
|
+
FROM memories m
|
|
778
|
+
LEFT JOIN provenance p ON m.id = p.memory_id
|
|
779
|
+
WHERE m.id IN (${placeholders})
|
|
780
|
+
ORDER BY m.created_at ASC
|
|
781
|
+
`).all(...ids);
|
|
782
|
+
|
|
783
|
+
return rows;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Search all memories FTS (including archived memories).
|
|
788
|
+
*/
|
|
789
|
+
export function searchAllMemoriesFts(queryText, limit = 10) {
|
|
790
|
+
try {
|
|
791
|
+
return stmts.searchFts.all(queryText, limit);
|
|
792
|
+
} catch (e) {
|
|
793
|
+
return [];
|
|
794
|
+
}
|
|
459
795
|
}
|
|
460
796
|
|
|
461
797
|
// ============================================================
|