atlas-mcp 0.1.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/.env.example +32 -0
- package/README.md +282 -0
- package/package.json +72 -0
- package/public/app/assets/app-CxbS1w9p.js +3981 -0
- package/public/app/assets/index-BA6nxCuI.css +1 -0
- package/public/app/assets/index-BXmIRrQH.js +177 -0
- package/public/app/index.html +27 -0
- package/public/assets/brain-atlas.LICENSE.txt +16 -0
- package/public/assets/brain-atlas.glb +0 -0
- package/public/assets/brain.obj +27282 -0
- package/public/fonts/DepartureMono-Regular.woff +0 -0
- package/public/fonts/DepartureMono-Regular.woff2 +0 -0
- package/scripts/sync-memory-vectors.js +46 -0
- package/src/audit.js +9 -0
- package/src/cli/args.js +87 -0
- package/src/cli/commands/add.js +103 -0
- package/src/cli/commands/config.js +228 -0
- package/src/cli/commands/delete.js +75 -0
- package/src/cli/commands/entities.js +39 -0
- package/src/cli/commands/entity.js +47 -0
- package/src/cli/commands/get.js +46 -0
- package/src/cli/commands/list.js +53 -0
- package/src/cli/commands/related.js +56 -0
- package/src/cli/commands/search.js +68 -0
- package/src/cli/commands/update.js +58 -0
- package/src/cli/deps.js +114 -0
- package/src/cli/env-file.js +44 -0
- package/src/cli/format.js +246 -0
- package/src/cli.js +187 -0
- package/src/cognitive-worker.js +381 -0
- package/src/db.js +2674 -0
- package/src/extraction-context.js +31 -0
- package/src/ingestion-service.js +387 -0
- package/src/ingestion-worker.js +225 -0
- package/src/llm-config.js +31 -0
- package/src/llm.js +789 -0
- package/src/logger.js +51 -0
- package/src/mcp-server.js +577 -0
- package/src/memory-comparison.js +421 -0
- package/src/related-memories.js +232 -0
- package/src/run-cognitive-worker.js +12 -0
- package/src/run-ingestion-worker.js +13 -0
- package/src/run-vector-worker.js +12 -0
- package/src/schemas.js +413 -0
- package/src/semantic-validation.js +430 -0
- package/src/server.js +827 -0
- package/src/shared/brain-regions.js +61 -0
- package/src/shared/entity-lens.js +249 -0
- package/src/shared/memory-placement.js +171 -0
- package/src/shared/memory-search.js +55 -0
- package/src/shared/region-anchors.js +112 -0
- package/src/shared/region-mapper.js +247 -0
- package/src/vector-store.js +546 -0
- package/src/vector-worker.js +71 -0
package/src/db.js
ADDED
|
@@ -0,0 +1,2674 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import {
|
|
5
|
+
REGION_MAPPING_VERSION,
|
|
6
|
+
mapExtractionToRegions,
|
|
7
|
+
} from "./shared/region-mapper.js";
|
|
8
|
+
import { EXTRACTION_SCHEMA_VERSION } from "./schemas.js";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const DB_PATH = process.env.ENGRAM_DB_PATH || join(__dirname, "..", "engram.db");
|
|
12
|
+
|
|
13
|
+
let db;
|
|
14
|
+
|
|
15
|
+
export function getDb() {
|
|
16
|
+
if (!db) {
|
|
17
|
+
db = new Database(DB_PATH);
|
|
18
|
+
db.pragma("journal_mode = WAL");
|
|
19
|
+
db.pragma("foreign_keys = ON");
|
|
20
|
+
initSchema();
|
|
21
|
+
}
|
|
22
|
+
return db;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function initSchema() {
|
|
26
|
+
db.exec(`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
raw_text TEXT NOT NULL,
|
|
30
|
+
ingestion_date TEXT NOT NULL,
|
|
31
|
+
summary TEXT,
|
|
32
|
+
type TEXT NOT NULL DEFAULT 'fact',
|
|
33
|
+
title TEXT NOT NULL DEFAULT '',
|
|
34
|
+
confidence REAL NOT NULL DEFAULT 0.6,
|
|
35
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
36
|
+
scope TEXT NOT NULL DEFAULT 'agent',
|
|
37
|
+
source TEXT NOT NULL DEFAULT 'ui',
|
|
38
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
39
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS memory_extractions (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
memory_id TEXT NOT NULL,
|
|
45
|
+
extraction_json TEXT NOT NULL,
|
|
46
|
+
model TEXT NOT NULL,
|
|
47
|
+
schema_version INTEGER NOT NULL DEFAULT 1,
|
|
48
|
+
authoritative INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
50
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
canonical_name TEXT NOT NULL,
|
|
56
|
+
kind TEXT NOT NULL CHECK (kind IN ('person', 'place', 'object', 'concept', 'organization')),
|
|
57
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
58
|
+
UNIQUE(canonical_name, kind)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
CREATE TABLE IF NOT EXISTS entity_aliases (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
entity_id INTEGER NOT NULL,
|
|
64
|
+
alias TEXT NOT NULL,
|
|
65
|
+
normalized_alias TEXT NOT NULL,
|
|
66
|
+
canonical INTEGER NOT NULL DEFAULT 0,
|
|
67
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
68
|
+
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
69
|
+
UNIQUE(entity_id, normalized_alias)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS entity_resolution_suggestions (
|
|
73
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
74
|
+
source_entity_id INTEGER NOT NULL,
|
|
75
|
+
target_entity_id INTEGER NOT NULL,
|
|
76
|
+
source_name TEXT NOT NULL,
|
|
77
|
+
target_name TEXT NOT NULL,
|
|
78
|
+
kind TEXT NOT NULL,
|
|
79
|
+
observed_alias TEXT NOT NULL,
|
|
80
|
+
normalized_alias TEXT NOT NULL,
|
|
81
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
82
|
+
CHECK (status IN ('pending', 'merged', 'rejected')),
|
|
83
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
84
|
+
resolved_at TEXT,
|
|
85
|
+
CHECK (source_entity_id <> target_entity_id),
|
|
86
|
+
UNIQUE(source_entity_id, target_entity_id)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS memory_entities (
|
|
90
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
91
|
+
memory_id TEXT NOT NULL,
|
|
92
|
+
entity_id INTEGER NOT NULL,
|
|
93
|
+
mention TEXT NOT NULL,
|
|
94
|
+
role TEXT,
|
|
95
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
96
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
97
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
98
|
+
FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
99
|
+
UNIQUE(memory_id, entity_id, mention)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS relationships (
|
|
103
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
|
+
source_entity_id INTEGER NOT NULL,
|
|
105
|
+
target_entity_id INTEGER NOT NULL,
|
|
106
|
+
predicate TEXT NOT NULL,
|
|
107
|
+
memory_id TEXT NOT NULL,
|
|
108
|
+
confidence REAL NOT NULL DEFAULT 1.0,
|
|
109
|
+
evidence TEXT,
|
|
110
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
111
|
+
FOREIGN KEY (source_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
112
|
+
FOREIGN KEY (target_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
113
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
114
|
+
UNIQUE(source_entity_id, target_entity_id, predicate, memory_id)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
CREATE TABLE IF NOT EXISTS region_activations (
|
|
118
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
119
|
+
memory_id TEXT NOT NULL,
|
|
120
|
+
region TEXT NOT NULL,
|
|
121
|
+
weight REAL NOT NULL,
|
|
122
|
+
left_weight REAL,
|
|
123
|
+
right_weight REAL,
|
|
124
|
+
mapping_version INTEGER NOT NULL DEFAULT 1,
|
|
125
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
126
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
127
|
+
UNIQUE(memory_id, region, mapping_version)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
CREATE TABLE IF NOT EXISTS memory_comparisons (
|
|
131
|
+
left_memory_id TEXT NOT NULL,
|
|
132
|
+
right_memory_id TEXT NOT NULL,
|
|
133
|
+
input_hash TEXT NOT NULL,
|
|
134
|
+
model TEXT NOT NULL,
|
|
135
|
+
schema_version INTEGER NOT NULL,
|
|
136
|
+
comparison_json TEXT NOT NULL,
|
|
137
|
+
generated_at TEXT NOT NULL,
|
|
138
|
+
PRIMARY KEY (
|
|
139
|
+
left_memory_id,
|
|
140
|
+
right_memory_id,
|
|
141
|
+
input_hash,
|
|
142
|
+
model,
|
|
143
|
+
schema_version
|
|
144
|
+
),
|
|
145
|
+
FOREIGN KEY (left_memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
146
|
+
FOREIGN KEY (right_memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
CREATE TABLE IF NOT EXISTS memory_revisions (
|
|
150
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
151
|
+
memory_id TEXT NOT NULL,
|
|
152
|
+
revision_number INTEGER NOT NULL,
|
|
153
|
+
snapshot_json TEXT NOT NULL,
|
|
154
|
+
created_at TEXT NOT NULL,
|
|
155
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
156
|
+
UNIQUE(memory_id, revision_number)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
CREATE TABLE IF NOT EXISTS memory_sources (
|
|
160
|
+
id TEXT PRIMARY KEY,
|
|
161
|
+
text TEXT NOT NULL,
|
|
162
|
+
source TEXT NOT NULL CHECK (source IN ('ui', 'mcp', 'cli', 'import')),
|
|
163
|
+
ingestion_date TEXT NOT NULL,
|
|
164
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
165
|
+
extraction_status TEXT NOT NULL DEFAULT 'pending'
|
|
166
|
+
CHECK (extraction_status IN ('pending', 'processing', 'completed', 'failed')),
|
|
167
|
+
extraction_attempts INTEGER NOT NULL DEFAULT 0,
|
|
168
|
+
extraction_error TEXT,
|
|
169
|
+
extraction_model TEXT,
|
|
170
|
+
extraction_schema_version INTEGER,
|
|
171
|
+
created_at TEXT NOT NULL,
|
|
172
|
+
updated_at TEXT NOT NULL
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
CREATE TABLE IF NOT EXISTS source_revisions (
|
|
176
|
+
id TEXT PRIMARY KEY,
|
|
177
|
+
source_id TEXT NOT NULL,
|
|
178
|
+
text TEXT NOT NULL,
|
|
179
|
+
author TEXT,
|
|
180
|
+
reason TEXT,
|
|
181
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
182
|
+
created_at TEXT NOT NULL,
|
|
183
|
+
FOREIGN KEY (source_id) REFERENCES memory_sources(id) ON DELETE CASCADE
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
CREATE TABLE IF NOT EXISTS source_memory_links (
|
|
187
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
188
|
+
source_id TEXT NOT NULL,
|
|
189
|
+
source_revision_id TEXT,
|
|
190
|
+
memory_id TEXT NOT NULL,
|
|
191
|
+
action TEXT NOT NULL CHECK (action IN ('created', 'updated', 'unchanged')),
|
|
192
|
+
evidence_json TEXT NOT NULL,
|
|
193
|
+
extraction_model TEXT,
|
|
194
|
+
extraction_schema_version INTEGER NOT NULL,
|
|
195
|
+
decision_confidence REAL NOT NULL,
|
|
196
|
+
decision_reason TEXT NOT NULL,
|
|
197
|
+
created_at TEXT NOT NULL,
|
|
198
|
+
FOREIGN KEY (source_id) REFERENCES memory_sources(id) ON DELETE CASCADE,
|
|
199
|
+
FOREIGN KEY (source_revision_id) REFERENCES source_revisions(id) ON DELETE SET NULL,
|
|
200
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
CREATE TABLE IF NOT EXISTS cognitive_annotations (
|
|
204
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
205
|
+
memory_id TEXT NOT NULL,
|
|
206
|
+
memory_version INTEGER NOT NULL,
|
|
207
|
+
annotation_json TEXT NOT NULL,
|
|
208
|
+
model TEXT NOT NULL,
|
|
209
|
+
schema_version INTEGER NOT NULL,
|
|
210
|
+
created_at TEXT NOT NULL,
|
|
211
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
212
|
+
UNIQUE(memory_id, memory_version, schema_version)
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
CREATE TABLE IF NOT EXISTS annotation_jobs (
|
|
216
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
217
|
+
memory_id TEXT NOT NULL,
|
|
218
|
+
source_id TEXT,
|
|
219
|
+
memory_version INTEGER NOT NULL,
|
|
220
|
+
model TEXT NOT NULL,
|
|
221
|
+
schema_version INTEGER NOT NULL,
|
|
222
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
223
|
+
CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
|
|
224
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
225
|
+
max_attempts INTEGER NOT NULL DEFAULT 5,
|
|
226
|
+
available_at TEXT NOT NULL,
|
|
227
|
+
claimed_at TEXT,
|
|
228
|
+
completed_at TEXT,
|
|
229
|
+
last_error TEXT,
|
|
230
|
+
created_at TEXT NOT NULL,
|
|
231
|
+
updated_at TEXT NOT NULL,
|
|
232
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
233
|
+
FOREIGN KEY (source_id) REFERENCES memory_sources(id) ON DELETE SET NULL,
|
|
234
|
+
UNIQUE(memory_id, memory_version, schema_version)
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
CREATE TABLE IF NOT EXISTS vector_index_jobs (
|
|
238
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
239
|
+
memory_id TEXT NOT NULL,
|
|
240
|
+
source_id TEXT,
|
|
241
|
+
memory_version INTEGER NOT NULL,
|
|
242
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
243
|
+
CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
|
|
244
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
245
|
+
max_attempts INTEGER NOT NULL DEFAULT 8,
|
|
246
|
+
available_at TEXT NOT NULL,
|
|
247
|
+
claimed_at TEXT,
|
|
248
|
+
completed_at TEXT,
|
|
249
|
+
last_error TEXT,
|
|
250
|
+
created_at TEXT NOT NULL,
|
|
251
|
+
updated_at TEXT NOT NULL,
|
|
252
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
253
|
+
FOREIGN KEY (source_id) REFERENCES memory_sources(id) ON DELETE SET NULL,
|
|
254
|
+
UNIQUE(memory_id, memory_version)
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
CREATE TABLE IF NOT EXISTS ingestion_jobs (
|
|
258
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
259
|
+
source_id TEXT NOT NULL,
|
|
260
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
261
|
+
CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
|
|
262
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
263
|
+
max_attempts INTEGER NOT NULL DEFAULT 5,
|
|
264
|
+
available_at TEXT NOT NULL,
|
|
265
|
+
claimed_at TEXT,
|
|
266
|
+
completed_at TEXT,
|
|
267
|
+
last_error TEXT,
|
|
268
|
+
created_at TEXT NOT NULL,
|
|
269
|
+
updated_at TEXT NOT NULL,
|
|
270
|
+
FOREIGN KEY (source_id) REFERENCES memory_sources(id) ON DELETE CASCADE,
|
|
271
|
+
UNIQUE(source_id)
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
CREATE INDEX IF NOT EXISTS idx_extractions_memory ON memory_extractions(memory_id);
|
|
275
|
+
CREATE INDEX IF NOT EXISTS idx_extractions_memory_latest
|
|
276
|
+
ON memory_extractions(memory_id, created_at DESC, id DESC);
|
|
277
|
+
CREATE INDEX IF NOT EXISTS idx_entities_kind ON entities(kind);
|
|
278
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(canonical_name);
|
|
279
|
+
CREATE INDEX IF NOT EXISTS idx_entity_aliases_entity
|
|
280
|
+
ON entity_aliases(entity_id);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_entity_aliases_normalized
|
|
282
|
+
ON entity_aliases(normalized_alias);
|
|
283
|
+
CREATE INDEX IF NOT EXISTS idx_entity_resolution_status
|
|
284
|
+
ON entity_resolution_suggestions(status, created_at DESC);
|
|
285
|
+
CREATE INDEX IF NOT EXISTS idx_memory_entities_memory ON memory_entities(memory_id);
|
|
286
|
+
CREATE INDEX IF NOT EXISTS idx_memory_entities_entity ON memory_entities(entity_id);
|
|
287
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_source ON relationships(source_entity_id);
|
|
288
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_target ON relationships(target_entity_id);
|
|
289
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_memory ON relationships(memory_id);
|
|
290
|
+
CREATE INDEX IF NOT EXISTS idx_region_activations_memory ON region_activations(memory_id);
|
|
291
|
+
CREATE INDEX IF NOT EXISTS idx_memory_comparisons_left ON memory_comparisons(left_memory_id);
|
|
292
|
+
CREATE INDEX IF NOT EXISTS idx_memory_comparisons_right ON memory_comparisons(right_memory_id);
|
|
293
|
+
CREATE INDEX IF NOT EXISTS idx_memory_revisions_memory
|
|
294
|
+
ON memory_revisions(memory_id, revision_number DESC);
|
|
295
|
+
CREATE INDEX IF NOT EXISTS idx_sources_status
|
|
296
|
+
ON memory_sources(extraction_status, updated_at);
|
|
297
|
+
CREATE INDEX IF NOT EXISTS idx_source_revisions_source
|
|
298
|
+
ON source_revisions(source_id, created_at DESC);
|
|
299
|
+
CREATE INDEX IF NOT EXISTS idx_source_links_source
|
|
300
|
+
ON source_memory_links(source_id, created_at ASC);
|
|
301
|
+
CREATE INDEX IF NOT EXISTS idx_source_links_memory
|
|
302
|
+
ON source_memory_links(memory_id, created_at DESC);
|
|
303
|
+
CREATE INDEX IF NOT EXISTS idx_annotation_jobs_claim
|
|
304
|
+
ON annotation_jobs(status, available_at, id);
|
|
305
|
+
CREATE INDEX IF NOT EXISTS idx_vector_jobs_claim
|
|
306
|
+
ON vector_index_jobs(status, available_at, id);
|
|
307
|
+
CREATE INDEX IF NOT EXISTS idx_ingestion_jobs_claim
|
|
308
|
+
ON ingestion_jobs(status, available_at, id);
|
|
309
|
+
`);
|
|
310
|
+
|
|
311
|
+
const memoryColumns = db.prepare("PRAGMA table_info(memories)").all();
|
|
312
|
+
const memoryMigrations = [
|
|
313
|
+
["source", "TEXT NOT NULL DEFAULT 'ui'"],
|
|
314
|
+
["type", "TEXT NOT NULL DEFAULT 'fact'"],
|
|
315
|
+
["title", "TEXT NOT NULL DEFAULT ''"],
|
|
316
|
+
["confidence", "REAL NOT NULL DEFAULT 0.6"],
|
|
317
|
+
["tags", "TEXT NOT NULL DEFAULT '[]'"],
|
|
318
|
+
["scope", "TEXT NOT NULL DEFAULT 'agent'"],
|
|
319
|
+
["version", "INTEGER NOT NULL DEFAULT 1"],
|
|
320
|
+
];
|
|
321
|
+
for (const [name, definition] of memoryMigrations) {
|
|
322
|
+
if (!memoryColumns.some((column) => column.name === name)) {
|
|
323
|
+
db.exec(`ALTER TABLE memories ADD COLUMN ${name} ${definition}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
db.exec(`
|
|
327
|
+
UPDATE memories
|
|
328
|
+
SET title = substr(raw_text, 1, 50)
|
|
329
|
+
WHERE title = ''
|
|
330
|
+
`);
|
|
331
|
+
|
|
332
|
+
const regionActivationColumns = db
|
|
333
|
+
.prepare("PRAGMA table_info(region_activations)")
|
|
334
|
+
.all();
|
|
335
|
+
if (!regionActivationColumns.some((column) => column.name === "left_weight")) {
|
|
336
|
+
db.exec("ALTER TABLE region_activations ADD COLUMN left_weight REAL");
|
|
337
|
+
}
|
|
338
|
+
if (!regionActivationColumns.some((column) => column.name === "right_weight")) {
|
|
339
|
+
db.exec("ALTER TABLE region_activations ADD COLUMN right_weight REAL");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
migrateSourceTables();
|
|
343
|
+
|
|
344
|
+
db.exec(`
|
|
345
|
+
CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source);
|
|
346
|
+
`);
|
|
347
|
+
|
|
348
|
+
mergeDuplicateEntities();
|
|
349
|
+
backfillCanonicalEntityAliases();
|
|
350
|
+
backfillLegacySources();
|
|
351
|
+
|
|
352
|
+
db.exec(`
|
|
353
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
354
|
+
memory_id UNINDEXED,
|
|
355
|
+
title,
|
|
356
|
+
summary,
|
|
357
|
+
raw_text,
|
|
358
|
+
tags,
|
|
359
|
+
tokenize='porter unicode61'
|
|
360
|
+
);
|
|
361
|
+
`);
|
|
362
|
+
syncMemoriesToFts();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function addMissingColumns(table, columns) {
|
|
366
|
+
const existingColumns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
367
|
+
for (const [name, definition] of columns) {
|
|
368
|
+
if (!existingColumns.some((column) => column.name === name)) {
|
|
369
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${definition}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function hasColumn(table, columnName) {
|
|
375
|
+
return db.prepare(`PRAGMA table_info(${table})`).all()
|
|
376
|
+
.some((column) => column.name === columnName);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function migrateSourceTables() {
|
|
380
|
+
addMissingColumns("memory_sources", [
|
|
381
|
+
["text", "TEXT NOT NULL DEFAULT ''"],
|
|
382
|
+
["source", "TEXT NOT NULL DEFAULT 'import'"],
|
|
383
|
+
["ingestion_date", "TEXT NOT NULL DEFAULT ''"],
|
|
384
|
+
["extraction_error", "TEXT"],
|
|
385
|
+
["extraction_model", "TEXT"],
|
|
386
|
+
["extraction_schema_version", "INTEGER"],
|
|
387
|
+
]);
|
|
388
|
+
addMissingColumns("source_revisions", [
|
|
389
|
+
["text", "TEXT NOT NULL DEFAULT ''"],
|
|
390
|
+
["author", "TEXT"],
|
|
391
|
+
]);
|
|
392
|
+
addMissingColumns("source_memory_links", [
|
|
393
|
+
["extraction_model", "TEXT"],
|
|
394
|
+
["extraction_schema_version", "INTEGER NOT NULL DEFAULT 1"],
|
|
395
|
+
["decision_confidence", "REAL NOT NULL DEFAULT 0"],
|
|
396
|
+
["decision_reason", "TEXT NOT NULL DEFAULT ''"],
|
|
397
|
+
]);
|
|
398
|
+
migrateAnnotationJobs();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function migrateAnnotationJobs() {
|
|
402
|
+
const columns = db.prepare("PRAGMA table_info(annotation_jobs)").all();
|
|
403
|
+
const currentColumns = ["source_id", "memory_version", "available_at"];
|
|
404
|
+
if (currentColumns.every((name) =>
|
|
405
|
+
columns.some((column) => column.name === name))) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
db.exec(`
|
|
410
|
+
DROP INDEX IF EXISTS idx_annotation_jobs_claim;
|
|
411
|
+
DROP INDEX IF EXISTS idx_annotation_jobs_memory;
|
|
412
|
+
ALTER TABLE annotation_jobs RENAME TO annotation_jobs_legacy;
|
|
413
|
+
|
|
414
|
+
CREATE TABLE annotation_jobs (
|
|
415
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
416
|
+
memory_id TEXT NOT NULL,
|
|
417
|
+
source_id TEXT,
|
|
418
|
+
memory_version INTEGER NOT NULL,
|
|
419
|
+
model TEXT NOT NULL,
|
|
420
|
+
schema_version INTEGER NOT NULL,
|
|
421
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
422
|
+
CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
|
|
423
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
424
|
+
max_attempts INTEGER NOT NULL DEFAULT 5,
|
|
425
|
+
available_at TEXT NOT NULL,
|
|
426
|
+
claimed_at TEXT,
|
|
427
|
+
completed_at TEXT,
|
|
428
|
+
last_error TEXT,
|
|
429
|
+
created_at TEXT NOT NULL,
|
|
430
|
+
updated_at TEXT NOT NULL,
|
|
431
|
+
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
|
|
432
|
+
FOREIGN KEY (source_id) REFERENCES memory_sources(id) ON DELETE SET NULL,
|
|
433
|
+
UNIQUE(memory_id, memory_version, schema_version)
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
INSERT OR IGNORE INTO annotation_jobs (
|
|
437
|
+
memory_id, source_id, memory_version, model, schema_version, status,
|
|
438
|
+
attempts, max_attempts, available_at, claimed_at, completed_at,
|
|
439
|
+
last_error, created_at, updated_at
|
|
440
|
+
)
|
|
441
|
+
SELECT
|
|
442
|
+
legacy.memory_id,
|
|
443
|
+
NULL,
|
|
444
|
+
COALESCE(memory.version, 1),
|
|
445
|
+
legacy.model,
|
|
446
|
+
legacy.schema_version,
|
|
447
|
+
CASE legacy.status WHEN 'retry' THEN 'pending' ELSE legacy.status END,
|
|
448
|
+
legacy.attempts,
|
|
449
|
+
legacy.max_attempts,
|
|
450
|
+
COALESCE(legacy.retry_at, legacy.created_at),
|
|
451
|
+
legacy.claimed_at,
|
|
452
|
+
legacy.completed_at,
|
|
453
|
+
legacy.last_error,
|
|
454
|
+
legacy.created_at,
|
|
455
|
+
legacy.updated_at
|
|
456
|
+
FROM annotation_jobs_legacy AS legacy
|
|
457
|
+
JOIN memories AS memory ON memory.id = legacy.memory_id
|
|
458
|
+
ORDER BY legacy.created_at, legacy.id;
|
|
459
|
+
|
|
460
|
+
DROP TABLE annotation_jobs_legacy;
|
|
461
|
+
CREATE INDEX idx_annotation_jobs_claim
|
|
462
|
+
ON annotation_jobs(status, available_at, id);
|
|
463
|
+
CREATE INDEX idx_annotation_jobs_memory
|
|
464
|
+
ON annotation_jobs(memory_id, created_at DESC);
|
|
465
|
+
`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function withTransaction(callback) {
|
|
469
|
+
return getDb().transaction(callback)();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// --- Immutable sources and durable work queues ---
|
|
473
|
+
|
|
474
|
+
export function createMemorySource({ id, text, source = "ui", ingestionDate, metadata = {} }) {
|
|
475
|
+
const now = new Date().toISOString();
|
|
476
|
+
const database = getDb();
|
|
477
|
+
if (hasColumn("memory_sources", "content")) {
|
|
478
|
+
database.prepare(`
|
|
479
|
+
INSERT INTO memory_sources (
|
|
480
|
+
id, text, content, source, ingestion_date, metadata_json, created_at, updated_at
|
|
481
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
482
|
+
`).run(id, text, text, source, ingestionDate, JSON.stringify(metadata), now, now);
|
|
483
|
+
} else {
|
|
484
|
+
database.prepare(`
|
|
485
|
+
INSERT INTO memory_sources (
|
|
486
|
+
id, text, source, ingestion_date, metadata_json, created_at, updated_at
|
|
487
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
488
|
+
`).run(id, text, source, ingestionDate, JSON.stringify(metadata), now, now);
|
|
489
|
+
}
|
|
490
|
+
return getMemorySource(id);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function getMemorySource(id, { includeRevisions = false } = {}) {
|
|
494
|
+
const database = getDb();
|
|
495
|
+
const row = database.prepare("SELECT * FROM memory_sources WHERE id = ?").get(id);
|
|
496
|
+
if (!row) return null;
|
|
497
|
+
const result = { ...row, metadata_json: parseJson(row.metadata_json, {}) };
|
|
498
|
+
if (includeRevisions) {
|
|
499
|
+
result.revisions = database.prepare(`
|
|
500
|
+
SELECT * FROM source_revisions
|
|
501
|
+
WHERE source_id = ? ORDER BY created_at DESC, id DESC
|
|
502
|
+
`).all(id).map((revision) => ({
|
|
503
|
+
...revision,
|
|
504
|
+
metadata_json: parseJson(revision.metadata_json, {}),
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export function createSourceRevision({
|
|
511
|
+
id, sourceId, text, author = null, reason = null, metadata = {},
|
|
512
|
+
}) {
|
|
513
|
+
const createdAt = new Date().toISOString();
|
|
514
|
+
const database = getDb();
|
|
515
|
+
if (hasColumn("source_revisions", "content")) {
|
|
516
|
+
const revisionNumber = database.prepare(`
|
|
517
|
+
SELECT COALESCE(MAX(revision_number), 0) + 1 AS value
|
|
518
|
+
FROM source_revisions WHERE source_id = ?
|
|
519
|
+
`).get(sourceId).value;
|
|
520
|
+
database.prepare(`
|
|
521
|
+
INSERT INTO source_revisions (
|
|
522
|
+
id, source_id, text, content, revision_number, author, reason,
|
|
523
|
+
metadata_json, created_at
|
|
524
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
525
|
+
`).run(id, sourceId, text, text, revisionNumber, author, reason,
|
|
526
|
+
JSON.stringify(metadata), createdAt);
|
|
527
|
+
} else {
|
|
528
|
+
database.prepare(`
|
|
529
|
+
INSERT INTO source_revisions (
|
|
530
|
+
id, source_id, text, author, reason, metadata_json, created_at
|
|
531
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
532
|
+
`).run(id, sourceId, text, author, reason, JSON.stringify(metadata), createdAt);
|
|
533
|
+
}
|
|
534
|
+
return database.prepare("SELECT * FROM source_revisions WHERE id = ?").get(id);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function updateMemorySourceStatus(id, status, {
|
|
538
|
+
incrementAttempts = false,
|
|
539
|
+
error = null,
|
|
540
|
+
model = null,
|
|
541
|
+
schemaVersion = null,
|
|
542
|
+
} = {}) {
|
|
543
|
+
const normalizedStatus = normalizeSourceStatus(status);
|
|
544
|
+
const updatedAt = new Date().toISOString();
|
|
545
|
+
getDb().prepare(`
|
|
546
|
+
UPDATE memory_sources
|
|
547
|
+
SET extraction_status = ?, extraction_attempts = extraction_attempts + ?,
|
|
548
|
+
extraction_error = ?, extraction_model = COALESCE(?, extraction_model),
|
|
549
|
+
extraction_schema_version = COALESCE(?, extraction_schema_version),
|
|
550
|
+
updated_at = ?
|
|
551
|
+
WHERE id = ?
|
|
552
|
+
`).run(normalizedStatus, incrementAttempts ? 1 : 0, error, model,
|
|
553
|
+
schemaVersion, updatedAt, id);
|
|
554
|
+
return getMemorySource(id);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function normalizeSourceStatus(status) {
|
|
558
|
+
if (status !== "failed" && status !== "extraction_failed") return status;
|
|
559
|
+
const tableSql = getDb().prepare(`
|
|
560
|
+
SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'memory_sources'
|
|
561
|
+
`).get()?.sql || "";
|
|
562
|
+
return tableSql.includes("'extraction_failed'") ? "extraction_failed" : "failed";
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function linkSourceMemory({
|
|
566
|
+
sourceId, sourceRevisionId = null, memoryId, action, evidence,
|
|
567
|
+
model = null, schemaVersion = EXTRACTION_SCHEMA_VERSION,
|
|
568
|
+
confidence = 0, reason = "",
|
|
569
|
+
}) {
|
|
570
|
+
const result = getDb().prepare(`
|
|
571
|
+
INSERT INTO source_memory_links (
|
|
572
|
+
source_id, source_revision_id, memory_id, action, evidence_json,
|
|
573
|
+
extraction_model, extraction_schema_version, decision_confidence,
|
|
574
|
+
decision_reason, created_at
|
|
575
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
576
|
+
`).run(sourceId, sourceRevisionId, memoryId, action,
|
|
577
|
+
JSON.stringify(evidence || []), model, schemaVersion, confidence, reason,
|
|
578
|
+
new Date().toISOString());
|
|
579
|
+
return result.lastInsertRowid;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function getSourceMemoryLinks(sourceId) {
|
|
583
|
+
return getDb().prepare(`
|
|
584
|
+
SELECT * FROM source_memory_links WHERE source_id = ? ORDER BY id ASC
|
|
585
|
+
`).all(sourceId).map((row) => ({
|
|
586
|
+
...row,
|
|
587
|
+
evidence_json: parseJson(row.evidence_json, []),
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function memoryVersion(memoryId) {
|
|
592
|
+
const memory = getDb().prepare("SELECT version FROM memories WHERE id = ?").get(memoryId);
|
|
593
|
+
if (!memory) throw new Error(`Memory not found: ${memoryId}`);
|
|
594
|
+
return memory.version;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function enqueueAnnotationJob({ memoryId, sourceId = null, model, schemaVersion }) {
|
|
598
|
+
const now = new Date().toISOString();
|
|
599
|
+
getDb().prepare(`
|
|
600
|
+
INSERT INTO annotation_jobs (
|
|
601
|
+
memory_id, source_id, memory_version, model, schema_version,
|
|
602
|
+
available_at, created_at, updated_at
|
|
603
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
604
|
+
ON CONFLICT(memory_id, memory_version, schema_version) DO NOTHING
|
|
605
|
+
`).run(memoryId, sourceId, memoryVersion(memoryId), model, schemaVersion,
|
|
606
|
+
now, now, now);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function enqueueVectorIndexJob({ memoryId, sourceId = null }) {
|
|
610
|
+
const now = new Date().toISOString();
|
|
611
|
+
getDb().prepare(`
|
|
612
|
+
INSERT INTO vector_index_jobs (
|
|
613
|
+
memory_id, source_id, memory_version, available_at, created_at, updated_at
|
|
614
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
615
|
+
ON CONFLICT(memory_id, memory_version) DO NOTHING
|
|
616
|
+
`).run(memoryId, sourceId, memoryVersion(memoryId), now, now, now);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export function enqueueIngestionJob({ sourceId }) {
|
|
620
|
+
const now = new Date().toISOString();
|
|
621
|
+
getDb().prepare(`
|
|
622
|
+
INSERT INTO ingestion_jobs (
|
|
623
|
+
source_id, available_at, created_at, updated_at
|
|
624
|
+
) VALUES (?, ?, ?, ?)
|
|
625
|
+
ON CONFLICT(source_id) DO NOTHING
|
|
626
|
+
`).run(sourceId, now, now, now);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function getIngestionStatus(sourceId) {
|
|
630
|
+
return getDb().prepare(`
|
|
631
|
+
SELECT status FROM ingestion_jobs WHERE source_id = ?
|
|
632
|
+
ORDER BY id DESC LIMIT 1
|
|
633
|
+
`).get(sourceId)?.status ?? null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function getAnnotationStatus(memoryId) {
|
|
637
|
+
return getDb().prepare(`
|
|
638
|
+
SELECT status FROM annotation_jobs WHERE memory_id = ?
|
|
639
|
+
ORDER BY memory_version DESC, id DESC LIMIT 1
|
|
640
|
+
`).get(memoryId)?.status ?? null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function getVectorIndexStatus(memoryId) {
|
|
644
|
+
return getDb().prepare(`
|
|
645
|
+
SELECT status FROM vector_index_jobs WHERE memory_id = ?
|
|
646
|
+
ORDER BY memory_version DESC, id DESC LIMIT 1
|
|
647
|
+
`).get(memoryId)?.status ?? null;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function claimJob(table, now) {
|
|
651
|
+
const database = getDb();
|
|
652
|
+
return database.transaction(() => {
|
|
653
|
+
const job = database.prepare(`
|
|
654
|
+
SELECT * FROM ${table} WHERE status = 'pending' AND available_at <= ?
|
|
655
|
+
ORDER BY available_at ASC, id ASC LIMIT 1
|
|
656
|
+
`).get(now);
|
|
657
|
+
if (!job) return null;
|
|
658
|
+
database.prepare(`
|
|
659
|
+
UPDATE ${table} SET status = 'processing', attempts = attempts + 1,
|
|
660
|
+
claimed_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'
|
|
661
|
+
`).run(now, now, job.id);
|
|
662
|
+
return database.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(job.id);
|
|
663
|
+
})();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function claimAnnotationJob({ now = new Date().toISOString() } = {}) {
|
|
667
|
+
return claimJob("annotation_jobs", now);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function claimVectorIndexJob({ now = new Date().toISOString() } = {}) {
|
|
671
|
+
return claimJob("vector_index_jobs", now);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export function claimIngestionJob({ now = new Date().toISOString() } = {}) {
|
|
675
|
+
return claimJob("ingestion_jobs", now);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function recoverJobs(table, { now, retryAt = now, staleBefore }) {
|
|
679
|
+
return getDb().prepare(`
|
|
680
|
+
UPDATE ${table} SET status = 'pending', available_at = ?, claimed_at = NULL,
|
|
681
|
+
updated_at = ? WHERE status = 'processing' AND claimed_at < ?
|
|
682
|
+
`).run(retryAt, now, staleBefore).changes;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export function recoverAnnotationJobs(options) {
|
|
686
|
+
return recoverJobs("annotation_jobs", options);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
export function recoverVectorIndexJobs(options) {
|
|
690
|
+
return recoverJobs("vector_index_jobs", options);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function recoverIngestionJobs(options) {
|
|
694
|
+
return recoverJobs("ingestion_jobs", options);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function retryJob(table, { jobId, error, retryAt, terminal = false, updatedAt }) {
|
|
698
|
+
getDb().prepare(`
|
|
699
|
+
UPDATE ${table} SET status = ?, available_at = ?, claimed_at = NULL,
|
|
700
|
+
last_error = ?, updated_at = ? WHERE id = ?
|
|
701
|
+
`).run(terminal ? "failed" : "pending", retryAt, error, updatedAt, jobId);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function retryAnnotationJob(options) {
|
|
705
|
+
return retryJob("annotation_jobs", options);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export function retryVectorIndexJob(options) {
|
|
709
|
+
return retryJob("vector_index_jobs", options);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export function retryIngestionJob(options) {
|
|
713
|
+
return retryJob("ingestion_jobs", options);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function completeJob(table, jobId, completedAt) {
|
|
717
|
+
getDb().prepare(`
|
|
718
|
+
UPDATE ${table} SET status = 'completed', completed_at = ?, claimed_at = NULL,
|
|
719
|
+
last_error = NULL, updated_at = ? WHERE id = ?
|
|
720
|
+
`).run(completedAt, completedAt, jobId);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function completeAnnotationJob({ jobId, completedAt }) {
|
|
724
|
+
return completeJob("annotation_jobs", jobId, completedAt);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
export function completeVectorIndexJob({ jobId, completedAt }) {
|
|
728
|
+
return completeJob("vector_index_jobs", jobId, completedAt);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function completeIngestionJob({ jobId, completedAt }) {
|
|
732
|
+
return completeJob("ingestion_jobs", jobId, completedAt);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function failJob(table, jobId, { error, failedAt }) {
|
|
736
|
+
getDb().prepare(`
|
|
737
|
+
UPDATE ${table} SET status = 'failed', last_error = ?, claimed_at = NULL,
|
|
738
|
+
updated_at = ? WHERE id = ?
|
|
739
|
+
`).run(error, failedAt, jobId);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export function failAnnotationJob(jobId, options) {
|
|
743
|
+
return failJob("annotation_jobs", jobId, options);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function failVectorIndexJob(jobId, options) {
|
|
747
|
+
return failJob("vector_index_jobs", jobId, options);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
export function failIngestionJob(jobId, options) {
|
|
751
|
+
return failJob("ingestion_jobs", jobId, options);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export function saveCognitiveAnnotation({ memoryId, annotation, model, schemaVersion }) {
|
|
755
|
+
const database = getDb();
|
|
756
|
+
database.prepare(`
|
|
757
|
+
INSERT INTO cognitive_annotations (
|
|
758
|
+
memory_id, memory_version, annotation_json, model, schema_version, created_at
|
|
759
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
760
|
+
ON CONFLICT(memory_id, memory_version, schema_version) DO UPDATE SET
|
|
761
|
+
annotation_json = excluded.annotation_json, model = excluded.model,
|
|
762
|
+
created_at = excluded.created_at
|
|
763
|
+
`).run(memoryId, memoryVersion(memoryId), JSON.stringify(annotation), model,
|
|
764
|
+
schemaVersion, new Date().toISOString());
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export function completeCognitiveAnnotation({
|
|
768
|
+
memoryId,
|
|
769
|
+
annotation,
|
|
770
|
+
activations,
|
|
771
|
+
mappingVersion,
|
|
772
|
+
model,
|
|
773
|
+
schemaVersion,
|
|
774
|
+
jobId,
|
|
775
|
+
completedAt,
|
|
776
|
+
}) {
|
|
777
|
+
return getDb().transaction(() => {
|
|
778
|
+
saveCognitiveAnnotation({ memoryId, annotation, model, schemaVersion });
|
|
779
|
+
saveRegionActivations(memoryId, activations, mappingVersion);
|
|
780
|
+
completeAnnotationJob({ jobId, completedAt });
|
|
781
|
+
})();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function parseJson(value, fallback) {
|
|
785
|
+
try {
|
|
786
|
+
return JSON.parse(value);
|
|
787
|
+
} catch {
|
|
788
|
+
return fallback;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function backfillLegacySources() {
|
|
793
|
+
const database = getDb();
|
|
794
|
+
const rows = database.prepare(`
|
|
795
|
+
SELECT m.id, m.raw_text, m.source, m.ingestion_date, m.created_at,
|
|
796
|
+
e.model, e.schema_version
|
|
797
|
+
FROM memories m
|
|
798
|
+
LEFT JOIN memory_extractions e ON e.id = (
|
|
799
|
+
SELECT latest.id FROM memory_extractions latest
|
|
800
|
+
WHERE latest.memory_id = m.id ORDER BY latest.id DESC LIMIT 1
|
|
801
|
+
)
|
|
802
|
+
WHERE NOT EXISTS (
|
|
803
|
+
SELECT 1 FROM source_memory_links link WHERE link.memory_id = m.id
|
|
804
|
+
)
|
|
805
|
+
`).all();
|
|
806
|
+
const migrate = database.transaction(() => {
|
|
807
|
+
for (const row of rows) {
|
|
808
|
+
const sourceId = `legacy:${row.id}`;
|
|
809
|
+
if (hasColumn("memory_sources", "content")) {
|
|
810
|
+
database.prepare(`
|
|
811
|
+
INSERT OR IGNORE INTO memory_sources (
|
|
812
|
+
id, text, content, source, ingestion_date, extraction_status,
|
|
813
|
+
extraction_attempts, extraction_model, extraction_schema_version,
|
|
814
|
+
created_at, updated_at
|
|
815
|
+
) VALUES (?, ?, ?, 'import', ?, 'completed', 0, ?, ?, ?, ?)
|
|
816
|
+
`).run(sourceId, row.raw_text, row.raw_text, row.ingestion_date,
|
|
817
|
+
row.model || "legacy", row.schema_version || 1, row.created_at, row.created_at);
|
|
818
|
+
} else {
|
|
819
|
+
database.prepare(`
|
|
820
|
+
INSERT OR IGNORE INTO memory_sources (
|
|
821
|
+
id, text, source, ingestion_date, extraction_status,
|
|
822
|
+
extraction_attempts, extraction_model, extraction_schema_version,
|
|
823
|
+
created_at, updated_at
|
|
824
|
+
) VALUES (?, ?, 'import', ?, 'completed', 0, ?, ?, ?, ?)
|
|
825
|
+
`).run(sourceId, row.raw_text, row.ingestion_date, row.model || "legacy",
|
|
826
|
+
row.schema_version || 1, row.created_at, row.created_at);
|
|
827
|
+
}
|
|
828
|
+
database.prepare(`
|
|
829
|
+
INSERT INTO source_memory_links (
|
|
830
|
+
source_id, memory_id, action, evidence_json, extraction_model,
|
|
831
|
+
extraction_schema_version, decision_confidence, decision_reason, created_at
|
|
832
|
+
) VALUES (?, ?, 'created', ?, ?, ?, 1, 'Synthetic legacy provenance', ?)
|
|
833
|
+
`).run(sourceId, row.id, JSON.stringify([{
|
|
834
|
+
start: 0,
|
|
835
|
+
end: row.raw_text.length,
|
|
836
|
+
text: row.raw_text,
|
|
837
|
+
}]), row.model || "legacy", row.schema_version || 1, row.created_at);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
migrate();
|
|
841
|
+
return rows.length;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// --- Memory CRUD ---
|
|
845
|
+
|
|
846
|
+
export function createMemory(
|
|
847
|
+
id,
|
|
848
|
+
rawText,
|
|
849
|
+
ingestionDate,
|
|
850
|
+
summary = null,
|
|
851
|
+
source = "ui",
|
|
852
|
+
metadata = {},
|
|
853
|
+
) {
|
|
854
|
+
const db = getDb();
|
|
855
|
+
const now = new Date().toISOString();
|
|
856
|
+
const title = metadata.title || String(rawText).slice(0, 50);
|
|
857
|
+
const tags = Array.isArray(metadata.tags) ? metadata.tags : [];
|
|
858
|
+
const stmt = db.prepare(`
|
|
859
|
+
INSERT INTO memories (
|
|
860
|
+
id,
|
|
861
|
+
raw_text,
|
|
862
|
+
ingestion_date,
|
|
863
|
+
summary,
|
|
864
|
+
type,
|
|
865
|
+
title,
|
|
866
|
+
confidence,
|
|
867
|
+
tags,
|
|
868
|
+
scope,
|
|
869
|
+
source,
|
|
870
|
+
created_at,
|
|
871
|
+
updated_at
|
|
872
|
+
)
|
|
873
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'agent', ?, ?, ?)
|
|
874
|
+
`);
|
|
875
|
+
stmt.run(
|
|
876
|
+
id,
|
|
877
|
+
rawText,
|
|
878
|
+
ingestionDate,
|
|
879
|
+
summary,
|
|
880
|
+
metadata.type || "fact",
|
|
881
|
+
title,
|
|
882
|
+
metadata.confidence ?? 0.6,
|
|
883
|
+
JSON.stringify(tags),
|
|
884
|
+
source,
|
|
885
|
+
now,
|
|
886
|
+
now,
|
|
887
|
+
);
|
|
888
|
+
syncFtsForMemory(id);
|
|
889
|
+
return getMemory(id);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
export function getMemory(id) {
|
|
893
|
+
const db = getDb();
|
|
894
|
+
return deserializeMemory(
|
|
895
|
+
db.prepare("SELECT * FROM memories WHERE id = ?").get(id),
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export function getMemoriesByIds(ids) {
|
|
900
|
+
if (!ids.length) return [];
|
|
901
|
+
const db = getDb();
|
|
902
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
903
|
+
return db
|
|
904
|
+
.prepare(`SELECT * FROM memories WHERE id IN (${placeholders})`)
|
|
905
|
+
.all(...ids)
|
|
906
|
+
.map(deserializeMemory);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export function getMemories({ limit = 100, offset = 0, source } = {}) {
|
|
910
|
+
const db = getDb();
|
|
911
|
+
if (source) {
|
|
912
|
+
return db
|
|
913
|
+
.prepare("SELECT * FROM memories WHERE source = ? ORDER BY created_at DESC LIMIT ? OFFSET ?")
|
|
914
|
+
.all(source, limit, offset)
|
|
915
|
+
.map(deserializeMemory);
|
|
916
|
+
}
|
|
917
|
+
return db
|
|
918
|
+
.prepare("SELECT * FROM memories ORDER BY created_at DESC LIMIT ? OFFSET ?")
|
|
919
|
+
.all(limit, offset)
|
|
920
|
+
.map(deserializeMemory);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const MEMORY_CATALOG_SORTS = Object.freeze({
|
|
924
|
+
title: "m.title",
|
|
925
|
+
type: "m.type",
|
|
926
|
+
source: "m.source",
|
|
927
|
+
confidence: "m.confidence",
|
|
928
|
+
created_at: "m.created_at",
|
|
929
|
+
linked: "linked_count",
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
export function getMemoryCatalog({
|
|
933
|
+
q = "",
|
|
934
|
+
limit = 25,
|
|
935
|
+
offset = 0,
|
|
936
|
+
sort = "created_at",
|
|
937
|
+
order = "desc",
|
|
938
|
+
source,
|
|
939
|
+
type,
|
|
940
|
+
} = {}) {
|
|
941
|
+
const db = getDb();
|
|
942
|
+
const conditions = [];
|
|
943
|
+
const params = {};
|
|
944
|
+
const normalizedQuery = String(q).trim();
|
|
945
|
+
|
|
946
|
+
if (normalizedQuery) {
|
|
947
|
+
conditions.push(`
|
|
948
|
+
(
|
|
949
|
+
m.title LIKE @query
|
|
950
|
+
OR m.summary LIKE @query
|
|
951
|
+
OR m.raw_text LIKE @query
|
|
952
|
+
OR EXISTS (
|
|
953
|
+
SELECT 1
|
|
954
|
+
FROM memory_entities search_me
|
|
955
|
+
JOIN entities search_e ON search_e.id = search_me.entity_id
|
|
956
|
+
WHERE search_me.memory_id = m.id
|
|
957
|
+
AND (
|
|
958
|
+
search_e.canonical_name LIKE @query
|
|
959
|
+
OR EXISTS (
|
|
960
|
+
SELECT 1
|
|
961
|
+
FROM entity_aliases search_ea
|
|
962
|
+
WHERE search_ea.entity_id = search_e.id
|
|
963
|
+
AND search_ea.alias LIKE @query
|
|
964
|
+
)
|
|
965
|
+
)
|
|
966
|
+
)
|
|
967
|
+
)
|
|
968
|
+
`);
|
|
969
|
+
params.query = `%${normalizedQuery}%`;
|
|
970
|
+
}
|
|
971
|
+
if (source) {
|
|
972
|
+
conditions.push("m.source = @source");
|
|
973
|
+
params.source = source;
|
|
974
|
+
}
|
|
975
|
+
if (type) {
|
|
976
|
+
conditions.push("m.type = @type");
|
|
977
|
+
params.type = type;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
981
|
+
const sortColumn = MEMORY_CATALOG_SORTS[sort] || MEMORY_CATALOG_SORTS.created_at;
|
|
982
|
+
const sortOrder = order === "asc" ? "ASC" : "DESC";
|
|
983
|
+
const count = db
|
|
984
|
+
.prepare(`SELECT COUNT(*) AS total FROM memories m ${where}`)
|
|
985
|
+
.get(params).total;
|
|
986
|
+
const rows = db
|
|
987
|
+
.prepare(`
|
|
988
|
+
SELECT
|
|
989
|
+
m.*,
|
|
990
|
+
COALESCE(
|
|
991
|
+
json_group_array(
|
|
992
|
+
CASE
|
|
993
|
+
WHEN e.id IS NULL THEN NULL
|
|
994
|
+
ELSE json_object(
|
|
995
|
+
'id', e.id,
|
|
996
|
+
'canonical_name', e.canonical_name,
|
|
997
|
+
'kind', e.kind,
|
|
998
|
+
'mention', me.mention,
|
|
999
|
+
'role', me.role,
|
|
1000
|
+
'confidence', me.confidence
|
|
1001
|
+
)
|
|
1002
|
+
END
|
|
1003
|
+
) FILTER (WHERE e.id IS NOT NULL),
|
|
1004
|
+
json('[]')
|
|
1005
|
+
) AS entities_json,
|
|
1006
|
+
(
|
|
1007
|
+
SELECT COUNT(DISTINCT cand.memory_id)
|
|
1008
|
+
FROM memory_entities cur
|
|
1009
|
+
JOIN memory_entities cand ON cand.entity_id = cur.entity_id AND cand.memory_id <> cur.memory_id
|
|
1010
|
+
WHERE cur.memory_id = m.id
|
|
1011
|
+
) AS linked_count
|
|
1012
|
+
FROM memories m
|
|
1013
|
+
LEFT JOIN memory_entities me ON me.memory_id = m.id
|
|
1014
|
+
LEFT JOIN entities e ON e.id = me.entity_id
|
|
1015
|
+
${where}
|
|
1016
|
+
GROUP BY m.id
|
|
1017
|
+
ORDER BY ${sortColumn} ${sortOrder}, m.id ASC
|
|
1018
|
+
LIMIT @limit OFFSET @offset
|
|
1019
|
+
`)
|
|
1020
|
+
.all({ ...params, limit, offset })
|
|
1021
|
+
.map(({ entities_json: entitiesJson, linked_count: linkedCount, ...memory }) => ({
|
|
1022
|
+
...deserializeMemory(memory),
|
|
1023
|
+
entities: JSON.parse(entitiesJson),
|
|
1024
|
+
linked_count: linkedCount || 0,
|
|
1025
|
+
}));
|
|
1026
|
+
|
|
1027
|
+
return { items: rows, total: count, limit, offset };
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
export function searchMemories(query, { limit = 20 } = {}) {
|
|
1031
|
+
const db = getDb();
|
|
1032
|
+
const pattern = `%${query}%`;
|
|
1033
|
+
return db
|
|
1034
|
+
.prepare(
|
|
1035
|
+
`SELECT *
|
|
1036
|
+
FROM memories
|
|
1037
|
+
WHERE raw_text LIKE ? OR summary LIKE ?
|
|
1038
|
+
ORDER BY created_at DESC
|
|
1039
|
+
LIMIT ?`
|
|
1040
|
+
)
|
|
1041
|
+
.all(pattern, pattern, limit)
|
|
1042
|
+
.map(deserializeMemory);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// --- FTS5 / BM25 ---
|
|
1046
|
+
|
|
1047
|
+
export function syncMemoriesToFts() {
|
|
1048
|
+
const database = getDb();
|
|
1049
|
+
database.exec("DELETE FROM memories_fts");
|
|
1050
|
+
database.prepare(`
|
|
1051
|
+
INSERT INTO memories_fts (memory_id, title, summary, raw_text, tags)
|
|
1052
|
+
SELECT id, title, summary, raw_text, tags FROM memories
|
|
1053
|
+
`).run();
|
|
1054
|
+
database.exec("INSERT INTO memories_fts(memories_fts) VALUES('optimize')");
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
export function indexMemoryFts(id) {
|
|
1058
|
+
const database = getDb();
|
|
1059
|
+
const memory = database.prepare("SELECT * FROM memories WHERE id = ?").get(id);
|
|
1060
|
+
if (!memory) return;
|
|
1061
|
+
database.prepare("DELETE FROM memories_fts WHERE memory_id = ?").run(id);
|
|
1062
|
+
database.exec("INSERT INTO memories_fts(memories_fts) VALUES('optimize')");
|
|
1063
|
+
database.prepare(`
|
|
1064
|
+
INSERT INTO memories_fts (memory_id, title, summary, raw_text, tags)
|
|
1065
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1066
|
+
`).run(id, memory.title || "", memory.summary || "", memory.raw_text || "", memory.tags || "[]");
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
export function removeMemoryFts(id) {
|
|
1070
|
+
const database = getDb();
|
|
1071
|
+
database.prepare("DELETE FROM memories_fts WHERE memory_id = ?").run(id);
|
|
1072
|
+
database.exec("INSERT INTO memories_fts(memories_fts) VALUES('optimize')");
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export function searchMemoriesFts(query, { limit = 20 } = {}) {
|
|
1076
|
+
const database = getDb();
|
|
1077
|
+
const ftsQuery = buildFtsQuery(query);
|
|
1078
|
+
if (!ftsQuery) return [];
|
|
1079
|
+
try {
|
|
1080
|
+
const rows = database.prepare(`
|
|
1081
|
+
SELECT memory_id, rank
|
|
1082
|
+
FROM memories_fts
|
|
1083
|
+
WHERE memories_fts MATCH ?
|
|
1084
|
+
ORDER BY rank
|
|
1085
|
+
LIMIT ?
|
|
1086
|
+
`).all(ftsQuery, limit);
|
|
1087
|
+
return rows.map((row) => ({ id: row.memory_id, score: row.rank }));
|
|
1088
|
+
} catch {
|
|
1089
|
+
return [];
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function buildFtsQuery(query) {
|
|
1094
|
+
const terms = String(query).trim().split(/\s+/).filter(Boolean);
|
|
1095
|
+
if (!terms.length) return "";
|
|
1096
|
+
return terms.map((t) => `"${t.replace(/"/g, '""')}"`).join(" OR ");
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function syncFtsForMemory(id) {
|
|
1100
|
+
try {
|
|
1101
|
+
indexMemoryFts(id);
|
|
1102
|
+
} catch {
|
|
1103
|
+
// FTS sync is best-effort; don't break the main operation
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function removeFtsForMemory(id) {
|
|
1108
|
+
try {
|
|
1109
|
+
removeMemoryFts(id);
|
|
1110
|
+
} catch {
|
|
1111
|
+
// FTS sync is best-effort
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
export function updateMemorySummary(id, summary) {
|
|
1116
|
+
const db = getDb();
|
|
1117
|
+
const updatedAt = new Date().toISOString();
|
|
1118
|
+
db.prepare(`
|
|
1119
|
+
UPDATE memories SET summary = ?, updated_at = ? WHERE id = ?
|
|
1120
|
+
`).run(summary, updatedAt, id);
|
|
1121
|
+
syncFtsForMemory(id);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// --- Memory Comparisons ---
|
|
1125
|
+
|
|
1126
|
+
export function getMemoryComparison({
|
|
1127
|
+
leftMemoryId,
|
|
1128
|
+
rightMemoryId,
|
|
1129
|
+
inputHash,
|
|
1130
|
+
model,
|
|
1131
|
+
schemaVersion,
|
|
1132
|
+
}) {
|
|
1133
|
+
const row = getDb()
|
|
1134
|
+
.prepare(`
|
|
1135
|
+
SELECT *
|
|
1136
|
+
FROM memory_comparisons
|
|
1137
|
+
WHERE left_memory_id = ?
|
|
1138
|
+
AND right_memory_id = ?
|
|
1139
|
+
AND input_hash = ?
|
|
1140
|
+
AND model = ?
|
|
1141
|
+
AND schema_version = ?
|
|
1142
|
+
`)
|
|
1143
|
+
.get(
|
|
1144
|
+
leftMemoryId,
|
|
1145
|
+
rightMemoryId,
|
|
1146
|
+
inputHash,
|
|
1147
|
+
model,
|
|
1148
|
+
schemaVersion,
|
|
1149
|
+
);
|
|
1150
|
+
if (!row) return null;
|
|
1151
|
+
return {
|
|
1152
|
+
...row,
|
|
1153
|
+
comparison_json: JSON.parse(row.comparison_json),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export function saveMemoryComparison({
|
|
1158
|
+
leftMemoryId,
|
|
1159
|
+
rightMemoryId,
|
|
1160
|
+
inputHash,
|
|
1161
|
+
model,
|
|
1162
|
+
schemaVersion,
|
|
1163
|
+
comparison,
|
|
1164
|
+
generatedAt = new Date().toISOString(),
|
|
1165
|
+
}) {
|
|
1166
|
+
const db = getDb();
|
|
1167
|
+
const save = db.transaction(() => {
|
|
1168
|
+
db.prepare(`
|
|
1169
|
+
DELETE FROM memory_comparisons
|
|
1170
|
+
WHERE left_memory_id = ?
|
|
1171
|
+
AND right_memory_id = ?
|
|
1172
|
+
AND model = ?
|
|
1173
|
+
AND schema_version = ?
|
|
1174
|
+
`).run(leftMemoryId, rightMemoryId, model, schemaVersion);
|
|
1175
|
+
db.prepare(`
|
|
1176
|
+
INSERT INTO memory_comparisons (
|
|
1177
|
+
left_memory_id,
|
|
1178
|
+
right_memory_id,
|
|
1179
|
+
input_hash,
|
|
1180
|
+
model,
|
|
1181
|
+
schema_version,
|
|
1182
|
+
comparison_json,
|
|
1183
|
+
generated_at
|
|
1184
|
+
)
|
|
1185
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1186
|
+
`).run(
|
|
1187
|
+
leftMemoryId,
|
|
1188
|
+
rightMemoryId,
|
|
1189
|
+
inputHash,
|
|
1190
|
+
model,
|
|
1191
|
+
schemaVersion,
|
|
1192
|
+
JSON.stringify(comparison),
|
|
1193
|
+
generatedAt,
|
|
1194
|
+
);
|
|
1195
|
+
});
|
|
1196
|
+
save();
|
|
1197
|
+
return getMemoryComparison({
|
|
1198
|
+
leftMemoryId,
|
|
1199
|
+
rightMemoryId,
|
|
1200
|
+
inputHash,
|
|
1201
|
+
model,
|
|
1202
|
+
schemaVersion,
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// --- Extraction CRUD ---
|
|
1207
|
+
|
|
1208
|
+
export function saveExtraction(
|
|
1209
|
+
memoryId,
|
|
1210
|
+
extractionJson,
|
|
1211
|
+
model,
|
|
1212
|
+
schemaVersion = EXTRACTION_SCHEMA_VERSION,
|
|
1213
|
+
) {
|
|
1214
|
+
const db = getDb();
|
|
1215
|
+
const stmt = db.prepare(`
|
|
1216
|
+
INSERT INTO memory_extractions (
|
|
1217
|
+
memory_id,
|
|
1218
|
+
extraction_json,
|
|
1219
|
+
model,
|
|
1220
|
+
schema_version
|
|
1221
|
+
)
|
|
1222
|
+
VALUES (?, ?, ?, ?)
|
|
1223
|
+
`);
|
|
1224
|
+
const result = stmt.run(
|
|
1225
|
+
memoryId,
|
|
1226
|
+
JSON.stringify(extractionJson),
|
|
1227
|
+
model,
|
|
1228
|
+
schemaVersion,
|
|
1229
|
+
);
|
|
1230
|
+
return result.lastInsertRowid;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
export function getExtractions(memoryId) {
|
|
1234
|
+
const db = getDb();
|
|
1235
|
+
const rows = db
|
|
1236
|
+
.prepare(
|
|
1237
|
+
`SELECT *
|
|
1238
|
+
FROM memory_extractions
|
|
1239
|
+
WHERE memory_id = ?
|
|
1240
|
+
ORDER BY created_at DESC, id DESC`
|
|
1241
|
+
)
|
|
1242
|
+
.all(memoryId);
|
|
1243
|
+
return rows.map((r) => ({ ...r, extraction_json: JSON.parse(r.extraction_json) }));
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
export function getLatestExtraction(memoryId) {
|
|
1247
|
+
const db = getDb();
|
|
1248
|
+
const row = db
|
|
1249
|
+
.prepare(
|
|
1250
|
+
`SELECT *
|
|
1251
|
+
FROM memory_extractions
|
|
1252
|
+
WHERE memory_id = ?
|
|
1253
|
+
ORDER BY created_at DESC, id DESC
|
|
1254
|
+
LIMIT 1`
|
|
1255
|
+
)
|
|
1256
|
+
.get(memoryId);
|
|
1257
|
+
if (!row) return null;
|
|
1258
|
+
return { ...row, extraction_json: JSON.parse(row.extraction_json) };
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
export function setExtractionAuthoritative(id) {
|
|
1262
|
+
const db = getDb();
|
|
1263
|
+
db.prepare("UPDATE memory_extractions SET authoritative = 1 WHERE id = ?").run(id);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// --- Entity CRUD ---
|
|
1267
|
+
|
|
1268
|
+
export function upsertEntity(canonicalName, kind) {
|
|
1269
|
+
const db = getDb();
|
|
1270
|
+
const normalizedName = normalizeEntityName(canonicalName);
|
|
1271
|
+
const matches = findExactEntityMatches(normalizedName, kind);
|
|
1272
|
+
if (matches.length === 1) {
|
|
1273
|
+
addEntityAlias(matches[0].id, normalizedName, true);
|
|
1274
|
+
return matches[0].id;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const result = db
|
|
1278
|
+
.prepare("INSERT INTO entities (canonical_name, kind) VALUES (?, ?)")
|
|
1279
|
+
.run(normalizedName, kind);
|
|
1280
|
+
addEntityAlias(result.lastInsertRowid, normalizedName, true);
|
|
1281
|
+
return result.lastInsertRowid;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
export function getEntity(id) {
|
|
1285
|
+
const db = getDb();
|
|
1286
|
+
return db.prepare("SELECT * FROM entities WHERE id = ?").get(id);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
export function findEntities(query) {
|
|
1290
|
+
const db = getDb();
|
|
1291
|
+
return db
|
|
1292
|
+
.prepare(
|
|
1293
|
+
`SELECT e.*
|
|
1294
|
+
FROM entities e
|
|
1295
|
+
WHERE e.canonical_name LIKE ?
|
|
1296
|
+
OR EXISTS (
|
|
1297
|
+
SELECT 1
|
|
1298
|
+
FROM entity_aliases ea
|
|
1299
|
+
WHERE ea.entity_id = e.id AND ea.alias LIKE ?
|
|
1300
|
+
)
|
|
1301
|
+
ORDER BY e.canonical_name`
|
|
1302
|
+
)
|
|
1303
|
+
.all(`%${query}%`, `%${query}%`);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
export function getEntityAliases(entityId) {
|
|
1307
|
+
return getDb()
|
|
1308
|
+
.prepare(`
|
|
1309
|
+
SELECT id, entity_id, alias, normalized_alias, canonical, created_at
|
|
1310
|
+
FROM entity_aliases
|
|
1311
|
+
WHERE entity_id = ?
|
|
1312
|
+
ORDER BY canonical DESC, alias COLLATE NOCASE ASC, id ASC
|
|
1313
|
+
`)
|
|
1314
|
+
.all(entityId);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
export function getEntityResolutionSuggestions({ status } = {}) {
|
|
1318
|
+
const db = getDb();
|
|
1319
|
+
const where = status ? "WHERE s.status = ?" : "";
|
|
1320
|
+
const params = status ? [status] : [];
|
|
1321
|
+
return db
|
|
1322
|
+
.prepare(`
|
|
1323
|
+
SELECT
|
|
1324
|
+
s.*,
|
|
1325
|
+
s.observed_alias AS alias,
|
|
1326
|
+
s.kind AS source_kind,
|
|
1327
|
+
s.kind AS target_kind,
|
|
1328
|
+
COALESCE(source.canonical_name, s.source_name) AS source_name,
|
|
1329
|
+
COALESCE(target.canonical_name, s.target_name) AS target_name
|
|
1330
|
+
FROM entity_resolution_suggestions s
|
|
1331
|
+
LEFT JOIN entities source ON source.id = s.source_entity_id
|
|
1332
|
+
LEFT JOIN entities target ON target.id = s.target_entity_id
|
|
1333
|
+
${where}
|
|
1334
|
+
ORDER BY s.created_at DESC, s.id DESC
|
|
1335
|
+
`)
|
|
1336
|
+
.all(...params);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
export function resolveEntityResolutionSuggestion(id, decision) {
|
|
1340
|
+
if (decision !== "merge" && decision !== "reject") {
|
|
1341
|
+
throw new Error(`Invalid entity resolution decision: ${decision}`);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const db = getDb();
|
|
1345
|
+
const resolve = db.transaction(() => {
|
|
1346
|
+
const suggestion = db
|
|
1347
|
+
.prepare("SELECT * FROM entity_resolution_suggestions WHERE id = ?")
|
|
1348
|
+
.get(id);
|
|
1349
|
+
if (!suggestion) return null;
|
|
1350
|
+
const desiredStatus = decision === "merge" ? "merged" : "rejected";
|
|
1351
|
+
if (suggestion.status !== "pending") {
|
|
1352
|
+
if (suggestion.status === desiredStatus) {
|
|
1353
|
+
return getEntityResolutionSuggestions().find((item) => item.id === id);
|
|
1354
|
+
}
|
|
1355
|
+
throw new Error(
|
|
1356
|
+
`Entity resolution suggestion ${id} is already ${suggestion.status}`,
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const resolvedAt = new Date().toISOString();
|
|
1361
|
+
if (decision === "reject") {
|
|
1362
|
+
db.prepare(`
|
|
1363
|
+
UPDATE entity_resolution_suggestions
|
|
1364
|
+
SET status = 'rejected', resolved_at = ?
|
|
1365
|
+
WHERE id = ?
|
|
1366
|
+
`).run(resolvedAt, id);
|
|
1367
|
+
return getEntityResolutionSuggestions().find((item) => item.id === id);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
mergeEntityIntoTarget(
|
|
1371
|
+
suggestion.source_entity_id,
|
|
1372
|
+
suggestion.target_entity_id,
|
|
1373
|
+
id,
|
|
1374
|
+
resolvedAt,
|
|
1375
|
+
);
|
|
1376
|
+
return getEntityResolutionSuggestions().find((item) => item.id === id);
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
return resolve();
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const ENTITY_CATALOG_SORTS = Object.freeze({
|
|
1383
|
+
canonical_name: "e.canonical_name",
|
|
1384
|
+
kind: "e.kind",
|
|
1385
|
+
memory_count: "memory_count",
|
|
1386
|
+
relationship_count: "relationship_count",
|
|
1387
|
+
created_at: "e.created_at",
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
export function getEntityCatalog({
|
|
1391
|
+
q = "",
|
|
1392
|
+
limit = 25,
|
|
1393
|
+
offset = 0,
|
|
1394
|
+
sort = "canonical_name",
|
|
1395
|
+
order = "asc",
|
|
1396
|
+
kind,
|
|
1397
|
+
} = {}) {
|
|
1398
|
+
const db = getDb();
|
|
1399
|
+
const conditions = [];
|
|
1400
|
+
const params = {};
|
|
1401
|
+
const normalizedQuery = String(q).trim();
|
|
1402
|
+
|
|
1403
|
+
if (normalizedQuery) {
|
|
1404
|
+
conditions.push(`
|
|
1405
|
+
(
|
|
1406
|
+
e.canonical_name LIKE @query
|
|
1407
|
+
OR EXISTS (
|
|
1408
|
+
SELECT 1
|
|
1409
|
+
FROM entity_aliases search_ea
|
|
1410
|
+
WHERE search_ea.entity_id = e.id
|
|
1411
|
+
AND search_ea.alias LIKE @query
|
|
1412
|
+
)
|
|
1413
|
+
)
|
|
1414
|
+
`);
|
|
1415
|
+
params.query = `%${normalizedQuery}%`;
|
|
1416
|
+
}
|
|
1417
|
+
if (kind) {
|
|
1418
|
+
conditions.push("e.kind = @kind");
|
|
1419
|
+
params.kind = kind;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1423
|
+
const sortColumn = ENTITY_CATALOG_SORTS[sort]
|
|
1424
|
+
|| ENTITY_CATALOG_SORTS.canonical_name;
|
|
1425
|
+
const sortOrder = order === "desc" ? "DESC" : "ASC";
|
|
1426
|
+
const total = db
|
|
1427
|
+
.prepare(`SELECT COUNT(*) AS total FROM entities e ${where}`)
|
|
1428
|
+
.get(params).total;
|
|
1429
|
+
const items = db
|
|
1430
|
+
.prepare(`
|
|
1431
|
+
SELECT
|
|
1432
|
+
e.*,
|
|
1433
|
+
COUNT(DISTINCT me.memory_id) AS memory_count,
|
|
1434
|
+
COUNT(DISTINCT r.id) AS relationship_count,
|
|
1435
|
+
(
|
|
1436
|
+
SELECT COUNT(*)
|
|
1437
|
+
FROM entity_aliases ea
|
|
1438
|
+
WHERE ea.entity_id = e.id
|
|
1439
|
+
) AS alias_count,
|
|
1440
|
+
(
|
|
1441
|
+
SELECT COUNT(*)
|
|
1442
|
+
FROM entity_resolution_suggestions ers
|
|
1443
|
+
WHERE ers.status = 'pending'
|
|
1444
|
+
AND (
|
|
1445
|
+
ers.source_entity_id = e.id
|
|
1446
|
+
OR ers.target_entity_id = e.id
|
|
1447
|
+
)
|
|
1448
|
+
) AS pending_suggestion_count
|
|
1449
|
+
FROM entities e
|
|
1450
|
+
LEFT JOIN memory_entities me ON me.entity_id = e.id
|
|
1451
|
+
LEFT JOIN relationships r
|
|
1452
|
+
ON r.source_entity_id = e.id OR r.target_entity_id = e.id
|
|
1453
|
+
${where}
|
|
1454
|
+
GROUP BY e.id
|
|
1455
|
+
ORDER BY ${sortColumn} ${sortOrder}, e.id ASC
|
|
1456
|
+
LIMIT @limit OFFSET @offset
|
|
1457
|
+
`)
|
|
1458
|
+
.all({ ...params, limit, offset });
|
|
1459
|
+
|
|
1460
|
+
return { items, total, limit, offset };
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// --- Memory-Entity Links ---
|
|
1464
|
+
|
|
1465
|
+
export function linkMemoryToEntity(memoryId, entityId, mention, role = null, confidence = 1.0) {
|
|
1466
|
+
const db = getDb();
|
|
1467
|
+
const stmt = db.prepare(`
|
|
1468
|
+
INSERT OR IGNORE INTO memory_entities (memory_id, entity_id, mention, role, confidence)
|
|
1469
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1470
|
+
`);
|
|
1471
|
+
stmt.run(memoryId, entityId, mention, role, confidence);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
export function getEntitiesForMemory(memoryId) {
|
|
1475
|
+
const db = getDb();
|
|
1476
|
+
return db
|
|
1477
|
+
.prepare(
|
|
1478
|
+
`SELECT e.*, me.mention, me.role, me.confidence
|
|
1479
|
+
FROM memory_entities me
|
|
1480
|
+
JOIN entities e ON e.id = me.entity_id
|
|
1481
|
+
WHERE me.memory_id = ?`
|
|
1482
|
+
)
|
|
1483
|
+
.all(memoryId);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
export function getMemoriesForEntity(entityId) {
|
|
1487
|
+
const db = getDb();
|
|
1488
|
+
return db
|
|
1489
|
+
.prepare(
|
|
1490
|
+
`SELECT m.*, me.mention, me.role, me.confidence AS entity_confidence
|
|
1491
|
+
FROM memory_entities me
|
|
1492
|
+
JOIN memories m ON m.id = me.memory_id
|
|
1493
|
+
WHERE me.entity_id = ?`
|
|
1494
|
+
)
|
|
1495
|
+
.all(entityId)
|
|
1496
|
+
.map(deserializeMemory);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// --- Relationships ---
|
|
1500
|
+
|
|
1501
|
+
export function addRelationship(sourceEntityId, targetEntityId, predicate, memoryId, confidence = 1.0, evidence = null) {
|
|
1502
|
+
const db = getDb();
|
|
1503
|
+
const stmt = db.prepare(`
|
|
1504
|
+
INSERT OR IGNORE INTO relationships (source_entity_id, target_entity_id, predicate, memory_id, confidence, evidence)
|
|
1505
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1506
|
+
`);
|
|
1507
|
+
stmt.run(sourceEntityId, targetEntityId, predicate, memoryId, confidence, evidence);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
export function getRelationshipsForMemory(memoryId) {
|
|
1511
|
+
const db = getDb();
|
|
1512
|
+
return db
|
|
1513
|
+
.prepare(
|
|
1514
|
+
`SELECT r.*,
|
|
1515
|
+
se.canonical_name AS source_name, se.kind AS source_kind,
|
|
1516
|
+
te.canonical_name AS target_name, te.kind AS target_kind
|
|
1517
|
+
FROM relationships r
|
|
1518
|
+
JOIN entities se ON se.id = r.source_entity_id
|
|
1519
|
+
JOIN entities te ON te.id = r.target_entity_id
|
|
1520
|
+
WHERE r.memory_id = ?`
|
|
1521
|
+
)
|
|
1522
|
+
.all(memoryId);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
export function getRelationshipsForEntity(entityId) {
|
|
1526
|
+
const db = getDb();
|
|
1527
|
+
return db
|
|
1528
|
+
.prepare(
|
|
1529
|
+
`SELECT r.*,
|
|
1530
|
+
se.canonical_name AS source_name, se.kind AS source_kind,
|
|
1531
|
+
te.canonical_name AS target_name, te.kind AS target_kind
|
|
1532
|
+
FROM relationships r
|
|
1533
|
+
JOIN entities se ON se.id = r.source_entity_id
|
|
1534
|
+
JOIN entities te ON te.id = r.target_entity_id
|
|
1535
|
+
WHERE r.source_entity_id = ? OR r.target_entity_id = ?
|
|
1536
|
+
ORDER BY r.created_at DESC, r.id DESC`
|
|
1537
|
+
)
|
|
1538
|
+
.all(entityId, entityId);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
export function getStructuralMemoryLinks(memoryId) {
|
|
1542
|
+
const db = getDb();
|
|
1543
|
+
const sharedEntityRows = db
|
|
1544
|
+
.prepare(`
|
|
1545
|
+
SELECT DISTINCT
|
|
1546
|
+
candidate.memory_id,
|
|
1547
|
+
e.id,
|
|
1548
|
+
e.canonical_name,
|
|
1549
|
+
e.kind
|
|
1550
|
+
FROM memory_entities current
|
|
1551
|
+
JOIN memory_entities candidate
|
|
1552
|
+
ON candidate.entity_id = current.entity_id
|
|
1553
|
+
AND candidate.memory_id <> current.memory_id
|
|
1554
|
+
JOIN entities e ON e.id = current.entity_id
|
|
1555
|
+
WHERE current.memory_id = ?
|
|
1556
|
+
ORDER BY candidate.memory_id ASC, e.canonical_name ASC, e.id ASC
|
|
1557
|
+
`)
|
|
1558
|
+
.all(memoryId);
|
|
1559
|
+
const sharedRelationshipRows = db
|
|
1560
|
+
.prepare(`
|
|
1561
|
+
SELECT DISTINCT
|
|
1562
|
+
candidate.memory_id,
|
|
1563
|
+
current.source_entity_id,
|
|
1564
|
+
source.canonical_name AS source_name,
|
|
1565
|
+
source.kind AS source_kind,
|
|
1566
|
+
current.predicate,
|
|
1567
|
+
current.target_entity_id,
|
|
1568
|
+
target.canonical_name AS target_name,
|
|
1569
|
+
target.kind AS target_kind
|
|
1570
|
+
FROM relationships current
|
|
1571
|
+
JOIN relationships candidate
|
|
1572
|
+
ON candidate.source_entity_id = current.source_entity_id
|
|
1573
|
+
AND candidate.target_entity_id = current.target_entity_id
|
|
1574
|
+
AND candidate.predicate = current.predicate
|
|
1575
|
+
AND candidate.memory_id <> current.memory_id
|
|
1576
|
+
JOIN entities source ON source.id = current.source_entity_id
|
|
1577
|
+
JOIN entities target ON target.id = current.target_entity_id
|
|
1578
|
+
WHERE current.memory_id = ?
|
|
1579
|
+
ORDER BY candidate.memory_id ASC, current.predicate ASC
|
|
1580
|
+
`)
|
|
1581
|
+
.all(memoryId);
|
|
1582
|
+
const candidates = new Map();
|
|
1583
|
+
const ensureCandidate = (candidateMemoryId) => {
|
|
1584
|
+
if (!candidates.has(candidateMemoryId)) {
|
|
1585
|
+
candidates.set(candidateMemoryId, {
|
|
1586
|
+
memory_id: candidateMemoryId,
|
|
1587
|
+
shared_entities: [],
|
|
1588
|
+
shared_relationships: [],
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
return candidates.get(candidateMemoryId);
|
|
1592
|
+
};
|
|
1593
|
+
|
|
1594
|
+
for (const { memory_id: candidateMemoryId, ...entity } of sharedEntityRows) {
|
|
1595
|
+
ensureCandidate(candidateMemoryId).shared_entities.push(entity);
|
|
1596
|
+
}
|
|
1597
|
+
for (const {
|
|
1598
|
+
memory_id: candidateMemoryId,
|
|
1599
|
+
...relationship
|
|
1600
|
+
} of sharedRelationshipRows) {
|
|
1601
|
+
ensureCandidate(candidateMemoryId).shared_relationships.push(relationship);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
return [...candidates.values()];
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// --- Region Activations ---
|
|
1608
|
+
|
|
1609
|
+
export function saveRegionActivations(
|
|
1610
|
+
memoryId,
|
|
1611
|
+
activations,
|
|
1612
|
+
mappingVersion = REGION_MAPPING_VERSION
|
|
1613
|
+
) {
|
|
1614
|
+
const db = getDb();
|
|
1615
|
+
const remove = db.prepare(
|
|
1616
|
+
"DELETE FROM region_activations WHERE memory_id = ? AND mapping_version = ?"
|
|
1617
|
+
);
|
|
1618
|
+
const stmt = db.prepare(`
|
|
1619
|
+
INSERT INTO region_activations (
|
|
1620
|
+
memory_id,
|
|
1621
|
+
region,
|
|
1622
|
+
weight,
|
|
1623
|
+
left_weight,
|
|
1624
|
+
right_weight,
|
|
1625
|
+
mapping_version
|
|
1626
|
+
)
|
|
1627
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1628
|
+
`);
|
|
1629
|
+
const insert = db.transaction((items) => {
|
|
1630
|
+
remove.run(memoryId, mappingVersion);
|
|
1631
|
+
for (const { region, weight, hemispheres } of items) {
|
|
1632
|
+
stmt.run(
|
|
1633
|
+
memoryId,
|
|
1634
|
+
region,
|
|
1635
|
+
weight,
|
|
1636
|
+
hemispheres?.left ?? null,
|
|
1637
|
+
hemispheres?.right ?? null,
|
|
1638
|
+
mappingVersion,
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
insert(activations);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
export function getRegionActivations(
|
|
1646
|
+
memoryId,
|
|
1647
|
+
mappingVersion = REGION_MAPPING_VERSION
|
|
1648
|
+
) {
|
|
1649
|
+
const db = getDb();
|
|
1650
|
+
return db
|
|
1651
|
+
.prepare(
|
|
1652
|
+
`SELECT region, weight, left_weight, right_weight, mapping_version
|
|
1653
|
+
FROM region_activations
|
|
1654
|
+
WHERE memory_id = ? AND mapping_version = ?
|
|
1655
|
+
ORDER BY weight DESC, region ASC`
|
|
1656
|
+
)
|
|
1657
|
+
.all(memoryId, mappingVersion)
|
|
1658
|
+
.map(({ left_weight: left, right_weight: right, ...activation }) => ({
|
|
1659
|
+
...activation,
|
|
1660
|
+
...(Number.isFinite(left) && Number.isFinite(right)
|
|
1661
|
+
? { hemispheres: { left, right } }
|
|
1662
|
+
: {}),
|
|
1663
|
+
}));
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
export function backfillRegionActivations() {
|
|
1667
|
+
const db = getDb();
|
|
1668
|
+
const rows = db
|
|
1669
|
+
.prepare(
|
|
1670
|
+
`SELECT m.id, e.extraction_json
|
|
1671
|
+
FROM memories m
|
|
1672
|
+
JOIN memory_extractions e ON e.id = (
|
|
1673
|
+
SELECT latest.id
|
|
1674
|
+
FROM memory_extractions latest
|
|
1675
|
+
WHERE latest.memory_id = m.id
|
|
1676
|
+
ORDER BY latest.created_at DESC, latest.id DESC
|
|
1677
|
+
LIMIT 1
|
|
1678
|
+
)
|
|
1679
|
+
WHERE NOT EXISTS (
|
|
1680
|
+
SELECT 1
|
|
1681
|
+
FROM region_activations ra
|
|
1682
|
+
WHERE ra.memory_id = m.id AND ra.mapping_version = ?
|
|
1683
|
+
)`
|
|
1684
|
+
)
|
|
1685
|
+
.all(REGION_MAPPING_VERSION);
|
|
1686
|
+
|
|
1687
|
+
const backfill = db.transaction(() => {
|
|
1688
|
+
for (const row of rows) {
|
|
1689
|
+
const extraction = JSON.parse(row.extraction_json);
|
|
1690
|
+
saveRegionActivations(
|
|
1691
|
+
row.id,
|
|
1692
|
+
mapExtractionToRegions(extraction),
|
|
1693
|
+
REGION_MAPPING_VERSION
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
backfill();
|
|
1698
|
+
|
|
1699
|
+
return rows.length;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// --- Full pipeline: extract + store ---
|
|
1703
|
+
|
|
1704
|
+
export function storeMemory(
|
|
1705
|
+
id,
|
|
1706
|
+
rawText,
|
|
1707
|
+
ingestionDate,
|
|
1708
|
+
extraction,
|
|
1709
|
+
model,
|
|
1710
|
+
source = "ui",
|
|
1711
|
+
metadata = {},
|
|
1712
|
+
) {
|
|
1713
|
+
const db = getDb();
|
|
1714
|
+
const storeAll = db.transaction(() => {
|
|
1715
|
+
createMemory(
|
|
1716
|
+
id,
|
|
1717
|
+
rawText,
|
|
1718
|
+
ingestionDate,
|
|
1719
|
+
extraction.summary,
|
|
1720
|
+
source,
|
|
1721
|
+
metadata,
|
|
1722
|
+
);
|
|
1723
|
+
saveExtraction(id, extraction, model);
|
|
1724
|
+
persistDerivedMemoryGraph(id, extraction);
|
|
1725
|
+
|
|
1726
|
+
return getMemory(id);
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
return storeAll();
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
export function updateMemoryGraph({
|
|
1733
|
+
memoryId,
|
|
1734
|
+
rawText,
|
|
1735
|
+
ingestionDate,
|
|
1736
|
+
extraction,
|
|
1737
|
+
model,
|
|
1738
|
+
metadata = {},
|
|
1739
|
+
schemaVersion = EXTRACTION_SCHEMA_VERSION,
|
|
1740
|
+
}) {
|
|
1741
|
+
const db = getDb();
|
|
1742
|
+
const updateAll = db.transaction(() => {
|
|
1743
|
+
const current = db
|
|
1744
|
+
.prepare("SELECT * FROM memories WHERE id = ?")
|
|
1745
|
+
.get(memoryId);
|
|
1746
|
+
if (!current) {
|
|
1747
|
+
throw new Error(`Memory not found: ${memoryId}`);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
const revisionNumber = db
|
|
1751
|
+
.prepare(`
|
|
1752
|
+
SELECT COALESCE(MAX(revision_number), 0) + 1 AS revision_number
|
|
1753
|
+
FROM memory_revisions
|
|
1754
|
+
WHERE memory_id = ?
|
|
1755
|
+
`)
|
|
1756
|
+
.get(memoryId).revision_number;
|
|
1757
|
+
const now = new Date().toISOString();
|
|
1758
|
+
const snapshot = getMemoryGraphSnapshot(memoryId);
|
|
1759
|
+
db.prepare(`
|
|
1760
|
+
INSERT INTO memory_revisions (
|
|
1761
|
+
memory_id,
|
|
1762
|
+
revision_number,
|
|
1763
|
+
snapshot_json,
|
|
1764
|
+
created_at
|
|
1765
|
+
)
|
|
1766
|
+
VALUES (?, ?, ?, ?)
|
|
1767
|
+
`).run(memoryId, revisionNumber, JSON.stringify(snapshot), now);
|
|
1768
|
+
|
|
1769
|
+
const tags = mergeTags(current.tags, metadata.tags);
|
|
1770
|
+
db.prepare(`
|
|
1771
|
+
UPDATE memories
|
|
1772
|
+
SET raw_text = ?,
|
|
1773
|
+
ingestion_date = ?,
|
|
1774
|
+
summary = ?,
|
|
1775
|
+
type = ?,
|
|
1776
|
+
title = ?,
|
|
1777
|
+
confidence = ?,
|
|
1778
|
+
tags = ?,
|
|
1779
|
+
version = version + 1,
|
|
1780
|
+
updated_at = ?
|
|
1781
|
+
WHERE id = ?
|
|
1782
|
+
`).run(
|
|
1783
|
+
rawText,
|
|
1784
|
+
ingestionDate,
|
|
1785
|
+
extraction.summary ?? null,
|
|
1786
|
+
metadata.type || "fact",
|
|
1787
|
+
metadata.title || String(rawText).slice(0, 50),
|
|
1788
|
+
metadata.confidence ?? 0.6,
|
|
1789
|
+
JSON.stringify(tags),
|
|
1790
|
+
now,
|
|
1791
|
+
memoryId,
|
|
1792
|
+
);
|
|
1793
|
+
|
|
1794
|
+
db.prepare("DELETE FROM relationships WHERE memory_id = ?").run(memoryId);
|
|
1795
|
+
db.prepare("DELETE FROM memory_entities WHERE memory_id = ?").run(memoryId);
|
|
1796
|
+
db.prepare("DELETE FROM region_activations WHERE memory_id = ?").run(memoryId);
|
|
1797
|
+
db.prepare(`
|
|
1798
|
+
DELETE FROM memory_comparisons
|
|
1799
|
+
WHERE left_memory_id = ? OR right_memory_id = ?
|
|
1800
|
+
`).run(memoryId, memoryId);
|
|
1801
|
+
|
|
1802
|
+
saveExtraction(memoryId, extraction, model, schemaVersion);
|
|
1803
|
+
persistDerivedMemoryGraph(memoryId, extraction);
|
|
1804
|
+
syncFtsForMemory(memoryId);
|
|
1805
|
+
|
|
1806
|
+
return {
|
|
1807
|
+
memory: getMemory(memoryId),
|
|
1808
|
+
revisionNumber,
|
|
1809
|
+
};
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
return updateAll();
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
export function getMemoryRevisions(memoryId) {
|
|
1816
|
+
return getDb()
|
|
1817
|
+
.prepare(`
|
|
1818
|
+
SELECT *
|
|
1819
|
+
FROM memory_revisions
|
|
1820
|
+
WHERE memory_id = ?
|
|
1821
|
+
ORDER BY revision_number DESC, id DESC
|
|
1822
|
+
`)
|
|
1823
|
+
.all(memoryId)
|
|
1824
|
+
.map((revision) => ({
|
|
1825
|
+
...revision,
|
|
1826
|
+
snapshot_json: JSON.parse(revision.snapshot_json),
|
|
1827
|
+
}));
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function getMemoryGraphSnapshot(memoryId) {
|
|
1831
|
+
const db = getDb();
|
|
1832
|
+
return {
|
|
1833
|
+
memory: deserializeMemory(
|
|
1834
|
+
db.prepare("SELECT * FROM memories WHERE id = ?").get(memoryId),
|
|
1835
|
+
),
|
|
1836
|
+
extractions: getExtractions(memoryId),
|
|
1837
|
+
entities: db
|
|
1838
|
+
.prepare(`
|
|
1839
|
+
SELECT e.*
|
|
1840
|
+
FROM entities e
|
|
1841
|
+
WHERE e.id IN (
|
|
1842
|
+
SELECT me.entity_id
|
|
1843
|
+
FROM memory_entities me
|
|
1844
|
+
WHERE me.memory_id = ?
|
|
1845
|
+
UNION
|
|
1846
|
+
SELECT r.source_entity_id
|
|
1847
|
+
FROM relationships r
|
|
1848
|
+
WHERE r.memory_id = ?
|
|
1849
|
+
UNION
|
|
1850
|
+
SELECT r.target_entity_id
|
|
1851
|
+
FROM relationships r
|
|
1852
|
+
WHERE r.memory_id = ?
|
|
1853
|
+
)
|
|
1854
|
+
ORDER BY e.id ASC
|
|
1855
|
+
`)
|
|
1856
|
+
.all(memoryId, memoryId, memoryId),
|
|
1857
|
+
entityLinks: db
|
|
1858
|
+
.prepare(`
|
|
1859
|
+
SELECT *
|
|
1860
|
+
FROM memory_entities
|
|
1861
|
+
WHERE memory_id = ?
|
|
1862
|
+
ORDER BY id ASC
|
|
1863
|
+
`)
|
|
1864
|
+
.all(memoryId),
|
|
1865
|
+
relationships: db
|
|
1866
|
+
.prepare(`
|
|
1867
|
+
SELECT *
|
|
1868
|
+
FROM relationships
|
|
1869
|
+
WHERE memory_id = ?
|
|
1870
|
+
ORDER BY id ASC
|
|
1871
|
+
`)
|
|
1872
|
+
.all(memoryId),
|
|
1873
|
+
regionActivations: db
|
|
1874
|
+
.prepare(`
|
|
1875
|
+
SELECT *
|
|
1876
|
+
FROM region_activations
|
|
1877
|
+
WHERE memory_id = ?
|
|
1878
|
+
ORDER BY mapping_version ASC, id ASC
|
|
1879
|
+
`)
|
|
1880
|
+
.all(memoryId),
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function persistDerivedMemoryGraph(memoryId, extraction) {
|
|
1885
|
+
saveRegionActivations(
|
|
1886
|
+
memoryId,
|
|
1887
|
+
mapExtractionToRegions(extraction),
|
|
1888
|
+
REGION_MAPPING_VERSION,
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
const entityIds = new Map();
|
|
1892
|
+
for (const ent of extraction.entities || []) {
|
|
1893
|
+
const name = ent.canonicalName || ent.mention;
|
|
1894
|
+
const entityId = resolveEntityForExtraction({
|
|
1895
|
+
canonicalName: name,
|
|
1896
|
+
mention: ent.mention,
|
|
1897
|
+
kind: ent.kind,
|
|
1898
|
+
});
|
|
1899
|
+
for (const alias of [name, ent.mention, ent.canonicalName]) {
|
|
1900
|
+
const key = normalizeEntityKey(alias);
|
|
1901
|
+
if (key) entityIds.set(key, entityId);
|
|
1902
|
+
}
|
|
1903
|
+
linkMemoryToEntity(
|
|
1904
|
+
memoryId,
|
|
1905
|
+
entityId,
|
|
1906
|
+
ent.mention,
|
|
1907
|
+
null,
|
|
1908
|
+
ent.confidence,
|
|
1909
|
+
);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
for (const rel of extraction.relationships || []) {
|
|
1913
|
+
const srcName = String(rel.subject || "").trim();
|
|
1914
|
+
const tgtName = String(rel.object || "").trim();
|
|
1915
|
+
const srcId =
|
|
1916
|
+
entityIds.get(normalizeEntityKey(srcName)) ||
|
|
1917
|
+
resolveEntityForExtraction({
|
|
1918
|
+
canonicalName: srcName,
|
|
1919
|
+
mention: srcName,
|
|
1920
|
+
kind: "concept",
|
|
1921
|
+
});
|
|
1922
|
+
const tgtId =
|
|
1923
|
+
entityIds.get(normalizeEntityKey(tgtName)) ||
|
|
1924
|
+
resolveEntityForExtraction({
|
|
1925
|
+
canonicalName: tgtName,
|
|
1926
|
+
mention: tgtName,
|
|
1927
|
+
kind: "concept",
|
|
1928
|
+
});
|
|
1929
|
+
addRelationship(
|
|
1930
|
+
srcId,
|
|
1931
|
+
tgtId,
|
|
1932
|
+
rel.predicate,
|
|
1933
|
+
memoryId,
|
|
1934
|
+
rel.confidence,
|
|
1935
|
+
rel.evidence || (rel.evidenceSpanIndexes || [])
|
|
1936
|
+
.map((index) => extraction.evidenceSpans?.[index]?.text)
|
|
1937
|
+
.filter(Boolean)
|
|
1938
|
+
.join(" … "),
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
function mergeTags(existingJson, incomingTags) {
|
|
1944
|
+
let existingTags = [];
|
|
1945
|
+
try {
|
|
1946
|
+
existingTags = JSON.parse(existingJson);
|
|
1947
|
+
} catch {
|
|
1948
|
+
existingTags = [];
|
|
1949
|
+
}
|
|
1950
|
+
return [
|
|
1951
|
+
...new Set([
|
|
1952
|
+
...(Array.isArray(existingTags) ? existingTags : []),
|
|
1953
|
+
...(Array.isArray(incomingTags) ? incomingTags : []),
|
|
1954
|
+
]),
|
|
1955
|
+
];
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function normalizeEntityKey(value) {
|
|
1959
|
+
return normalizeEntityName(value)
|
|
1960
|
+
.toLocaleLowerCase()
|
|
1961
|
+
.replace(/[\p{P}\p{S}]+/gu, " ")
|
|
1962
|
+
.replace(/\s+/g, " ")
|
|
1963
|
+
.trim();
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
function deserializeMemory(memory) {
|
|
1967
|
+
if (!memory) return memory;
|
|
1968
|
+
try {
|
|
1969
|
+
return { ...memory, tags: JSON.parse(memory.tags) };
|
|
1970
|
+
} catch {
|
|
1971
|
+
return { ...memory, tags: [] };
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function normalizeEntityName(value) {
|
|
1976
|
+
return String(value || "")
|
|
1977
|
+
.normalize("NFKC")
|
|
1978
|
+
.trim()
|
|
1979
|
+
.replace(/\s+/g, " ");
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
function mergeDuplicateEntities() {
|
|
1983
|
+
const entities = db
|
|
1984
|
+
.prepare("SELECT id, canonical_name, kind FROM entities ORDER BY id")
|
|
1985
|
+
.all();
|
|
1986
|
+
const canonicalIds = new Map();
|
|
1987
|
+
const merge = db.transaction(() => {
|
|
1988
|
+
const copyMemoryLinks = db.prepare(`
|
|
1989
|
+
INSERT OR IGNORE INTO memory_entities (
|
|
1990
|
+
memory_id,
|
|
1991
|
+
entity_id,
|
|
1992
|
+
mention,
|
|
1993
|
+
role,
|
|
1994
|
+
confidence,
|
|
1995
|
+
created_at
|
|
1996
|
+
)
|
|
1997
|
+
SELECT memory_id, ?, mention, role, confidence, created_at
|
|
1998
|
+
FROM memory_entities
|
|
1999
|
+
WHERE entity_id = ?
|
|
2000
|
+
`);
|
|
2001
|
+
const copyRelationships = db.prepare(`
|
|
2002
|
+
INSERT OR IGNORE INTO relationships (
|
|
2003
|
+
source_entity_id,
|
|
2004
|
+
target_entity_id,
|
|
2005
|
+
predicate,
|
|
2006
|
+
memory_id,
|
|
2007
|
+
confidence,
|
|
2008
|
+
evidence,
|
|
2009
|
+
created_at
|
|
2010
|
+
)
|
|
2011
|
+
SELECT
|
|
2012
|
+
CASE WHEN source_entity_id = ? THEN ? ELSE source_entity_id END,
|
|
2013
|
+
CASE WHEN target_entity_id = ? THEN ? ELSE target_entity_id END,
|
|
2014
|
+
predicate,
|
|
2015
|
+
memory_id,
|
|
2016
|
+
confidence,
|
|
2017
|
+
evidence,
|
|
2018
|
+
created_at
|
|
2019
|
+
FROM relationships
|
|
2020
|
+
WHERE source_entity_id = ? OR target_entity_id = ?
|
|
2021
|
+
`);
|
|
2022
|
+
const copyAliases = db.prepare(`
|
|
2023
|
+
INSERT OR IGNORE INTO entity_aliases (
|
|
2024
|
+
entity_id,
|
|
2025
|
+
alias,
|
|
2026
|
+
normalized_alias,
|
|
2027
|
+
canonical,
|
|
2028
|
+
created_at
|
|
2029
|
+
)
|
|
2030
|
+
SELECT ?, alias, normalized_alias, 0, created_at
|
|
2031
|
+
FROM entity_aliases
|
|
2032
|
+
WHERE entity_id = ?
|
|
2033
|
+
`);
|
|
2034
|
+
const deleteMemoryLinks = db.prepare(
|
|
2035
|
+
"DELETE FROM memory_entities WHERE entity_id = ?",
|
|
2036
|
+
);
|
|
2037
|
+
const deleteRelationships = db.prepare(
|
|
2038
|
+
"DELETE FROM relationships WHERE source_entity_id = ? OR target_entity_id = ?",
|
|
2039
|
+
);
|
|
2040
|
+
const closeSuggestions = db.prepare(`
|
|
2041
|
+
UPDATE entity_resolution_suggestions
|
|
2042
|
+
SET status = 'rejected', resolved_at = ?
|
|
2043
|
+
WHERE status = 'pending'
|
|
2044
|
+
AND (
|
|
2045
|
+
source_entity_id = ?
|
|
2046
|
+
OR target_entity_id = ?
|
|
2047
|
+
)
|
|
2048
|
+
`);
|
|
2049
|
+
const deleteEntity = db.prepare("DELETE FROM entities WHERE id = ?");
|
|
2050
|
+
|
|
2051
|
+
for (const entity of entities) {
|
|
2052
|
+
const key = `${entity.kind}:${normalizeEntityKey(entity.canonical_name)}`;
|
|
2053
|
+
const canonicalId = canonicalIds.get(key);
|
|
2054
|
+
if (!canonicalId) {
|
|
2055
|
+
canonicalIds.set(key, entity.id);
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
copyMemoryLinks.run(canonicalId, entity.id);
|
|
2060
|
+
copyAliases.run(canonicalId, entity.id);
|
|
2061
|
+
copyRelationships.run(
|
|
2062
|
+
entity.id,
|
|
2063
|
+
canonicalId,
|
|
2064
|
+
entity.id,
|
|
2065
|
+
canonicalId,
|
|
2066
|
+
entity.id,
|
|
2067
|
+
entity.id,
|
|
2068
|
+
);
|
|
2069
|
+
deleteMemoryLinks.run(entity.id);
|
|
2070
|
+
deleteRelationships.run(entity.id, entity.id);
|
|
2071
|
+
closeSuggestions.run(new Date().toISOString(), entity.id, entity.id);
|
|
2072
|
+
deleteEntity.run(entity.id);
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
merge();
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
const GENERIC_ENTITY_ALIASES = new Set([
|
|
2080
|
+
"he",
|
|
2081
|
+
"her",
|
|
2082
|
+
"hers",
|
|
2083
|
+
"herself",
|
|
2084
|
+
"him",
|
|
2085
|
+
"himself",
|
|
2086
|
+
"i",
|
|
2087
|
+
"it",
|
|
2088
|
+
"itself",
|
|
2089
|
+
"colleague",
|
|
2090
|
+
"company",
|
|
2091
|
+
"concept",
|
|
2092
|
+
"coworker",
|
|
2093
|
+
"friend",
|
|
2094
|
+
"girl",
|
|
2095
|
+
"guy",
|
|
2096
|
+
"man",
|
|
2097
|
+
"me",
|
|
2098
|
+
"myself",
|
|
2099
|
+
"object",
|
|
2100
|
+
"organization",
|
|
2101
|
+
"person",
|
|
2102
|
+
"place",
|
|
2103
|
+
"someone",
|
|
2104
|
+
"somebody",
|
|
2105
|
+
"something",
|
|
2106
|
+
"she",
|
|
2107
|
+
"them",
|
|
2108
|
+
"themselves",
|
|
2109
|
+
"they",
|
|
2110
|
+
"this person",
|
|
2111
|
+
"we",
|
|
2112
|
+
"who",
|
|
2113
|
+
"whom",
|
|
2114
|
+
"woman",
|
|
2115
|
+
"you",
|
|
2116
|
+
"yourself",
|
|
2117
|
+
]);
|
|
2118
|
+
|
|
2119
|
+
function isUsefulEntityAlias(value) {
|
|
2120
|
+
const key = normalizeEntityKey(value);
|
|
2121
|
+
return Boolean(key) && !GENERIC_ENTITY_ALIASES.has(key);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
function addEntityAlias(entityId, alias, canonical = false) {
|
|
2125
|
+
const displayAlias = normalizeEntityName(alias);
|
|
2126
|
+
const normalizedAlias = normalizeEntityKey(displayAlias);
|
|
2127
|
+
if (!normalizedAlias) return;
|
|
2128
|
+
getDb().prepare(`
|
|
2129
|
+
INSERT INTO entity_aliases (
|
|
2130
|
+
entity_id,
|
|
2131
|
+
alias,
|
|
2132
|
+
normalized_alias,
|
|
2133
|
+
canonical
|
|
2134
|
+
)
|
|
2135
|
+
VALUES (?, ?, ?, ?)
|
|
2136
|
+
ON CONFLICT(entity_id, normalized_alias) DO UPDATE SET
|
|
2137
|
+
canonical = MAX(entity_aliases.canonical, excluded.canonical)
|
|
2138
|
+
`).run(entityId, displayAlias, normalizedAlias, canonical ? 1 : 0);
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function backfillCanonicalEntityAliases() {
|
|
2142
|
+
const entities = db
|
|
2143
|
+
.prepare("SELECT id, canonical_name FROM entities ORDER BY id")
|
|
2144
|
+
.all();
|
|
2145
|
+
const backfill = db.transaction(() => {
|
|
2146
|
+
for (const entity of entities) {
|
|
2147
|
+
addEntityAlias(entity.id, entity.canonical_name, true);
|
|
2148
|
+
}
|
|
2149
|
+
});
|
|
2150
|
+
backfill();
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
function findExactEntityMatches(value, kind) {
|
|
2154
|
+
const key = normalizeEntityKey(value);
|
|
2155
|
+
if (!key) return [];
|
|
2156
|
+
return getDb()
|
|
2157
|
+
.prepare(`
|
|
2158
|
+
SELECT DISTINCT e.id, e.canonical_name, e.kind
|
|
2159
|
+
FROM entities e
|
|
2160
|
+
LEFT JOIN entity_aliases ea ON ea.entity_id = e.id
|
|
2161
|
+
WHERE e.kind = ?
|
|
2162
|
+
AND (
|
|
2163
|
+
ea.normalized_alias = ?
|
|
2164
|
+
OR lower(trim(e.canonical_name)) = ?
|
|
2165
|
+
)
|
|
2166
|
+
ORDER BY e.id ASC
|
|
2167
|
+
`)
|
|
2168
|
+
.all(kind, key, key);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
function resolveEntityForExtraction({ canonicalName, mention, kind }) {
|
|
2172
|
+
const db = getDb();
|
|
2173
|
+
const canonical = normalizeEntityName(canonicalName || mention);
|
|
2174
|
+
const canonicalMatches = findExactEntityMatches(canonical, kind);
|
|
2175
|
+
if (canonicalMatches.length === 1) {
|
|
2176
|
+
const entityId = canonicalMatches[0].id;
|
|
2177
|
+
addEntityAlias(entityId, canonical, true);
|
|
2178
|
+
recordMentionAliasOrSuggestions(entityId, mention, kind);
|
|
2179
|
+
return entityId;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
const mentionMatches = isUsefulEntityAlias(mention)
|
|
2183
|
+
? findExactEntityMatches(mention, kind)
|
|
2184
|
+
: [];
|
|
2185
|
+
if (canonicalMatches.length === 0 && mentionMatches.length === 1) {
|
|
2186
|
+
const entityId = mentionMatches[0].id;
|
|
2187
|
+
addEntityAlias(entityId, canonical, false);
|
|
2188
|
+
addEntityAlias(entityId, mention, false);
|
|
2189
|
+
return entityId;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
const result = db
|
|
2193
|
+
.prepare("INSERT INTO entities (canonical_name, kind) VALUES (?, ?)")
|
|
2194
|
+
.run(canonical, kind);
|
|
2195
|
+
const entityId = result.lastInsertRowid;
|
|
2196
|
+
addEntityAlias(entityId, canonical, true);
|
|
2197
|
+
if (isUsefulEntityAlias(mention)) {
|
|
2198
|
+
addEntityAlias(entityId, mention, false);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
const exactCandidates = new Map(
|
|
2202
|
+
[...canonicalMatches, ...mentionMatches].map((candidate) => [
|
|
2203
|
+
candidate.id,
|
|
2204
|
+
candidate,
|
|
2205
|
+
]),
|
|
2206
|
+
);
|
|
2207
|
+
for (const candidate of findFuzzyEntityCandidates(
|
|
2208
|
+
[canonical, mention],
|
|
2209
|
+
kind,
|
|
2210
|
+
entityId,
|
|
2211
|
+
)) {
|
|
2212
|
+
exactCandidates.set(candidate.id, candidate);
|
|
2213
|
+
}
|
|
2214
|
+
for (const candidate of exactCandidates.values()) {
|
|
2215
|
+
createEntityResolutionSuggestion(
|
|
2216
|
+
entityId,
|
|
2217
|
+
candidate.id,
|
|
2218
|
+
isUsefulEntityAlias(mention) ? mention : canonical,
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
return entityId;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
function recordMentionAliasOrSuggestions(entityId, mention, kind) {
|
|
2225
|
+
if (!isUsefulEntityAlias(mention)) return;
|
|
2226
|
+
const conflictingMatches = findExactEntityMatches(mention, kind)
|
|
2227
|
+
.filter((candidate) => candidate.id !== entityId);
|
|
2228
|
+
if (conflictingMatches.length === 0) {
|
|
2229
|
+
addEntityAlias(entityId, mention, false);
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
for (const candidate of conflictingMatches) {
|
|
2233
|
+
createEntityResolutionSuggestion(entityId, candidate.id, mention);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function findFuzzyEntityCandidates(values, kind, excludedEntityId) {
|
|
2238
|
+
const keys = [...new Set(values.map(normalizeEntityKey).filter(Boolean))];
|
|
2239
|
+
if (keys.length === 0) return [];
|
|
2240
|
+
const rows = getDb()
|
|
2241
|
+
.prepare(`
|
|
2242
|
+
SELECT DISTINCT
|
|
2243
|
+
e.id,
|
|
2244
|
+
e.canonical_name,
|
|
2245
|
+
e.kind,
|
|
2246
|
+
ea.normalized_alias
|
|
2247
|
+
FROM entities e
|
|
2248
|
+
JOIN entity_aliases ea ON ea.entity_id = e.id
|
|
2249
|
+
WHERE e.kind = ? AND e.id <> ?
|
|
2250
|
+
ORDER BY e.id ASC
|
|
2251
|
+
`)
|
|
2252
|
+
.all(kind, excludedEntityId);
|
|
2253
|
+
const matches = new Map();
|
|
2254
|
+
for (const row of rows) {
|
|
2255
|
+
if (keys.some((key) => areFuzzyEntityKeys(key, row.normalized_alias))) {
|
|
2256
|
+
matches.set(row.id, {
|
|
2257
|
+
id: row.id,
|
|
2258
|
+
canonical_name: row.canonical_name,
|
|
2259
|
+
kind: row.kind,
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
return [...matches.values()];
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
function areFuzzyEntityKeys(left, right) {
|
|
2267
|
+
if (!left || !right || left === right) return false;
|
|
2268
|
+
const leftTokens = left.split(" ");
|
|
2269
|
+
const rightTokens = right.split(" ");
|
|
2270
|
+
const shorter = leftTokens.length <= rightTokens.length
|
|
2271
|
+
? leftTokens
|
|
2272
|
+
: rightTokens;
|
|
2273
|
+
const longer = new Set(
|
|
2274
|
+
leftTokens.length <= rightTokens.length ? rightTokens : leftTokens,
|
|
2275
|
+
);
|
|
2276
|
+
return shorter.length > 0 && shorter.every((token) => longer.has(token));
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
function createEntityResolutionSuggestion(
|
|
2280
|
+
sourceEntityId,
|
|
2281
|
+
targetEntityId,
|
|
2282
|
+
observedAlias,
|
|
2283
|
+
) {
|
|
2284
|
+
if (sourceEntityId === targetEntityId) return;
|
|
2285
|
+
const db = getDb();
|
|
2286
|
+
const source = db
|
|
2287
|
+
.prepare("SELECT canonical_name, kind FROM entities WHERE id = ?")
|
|
2288
|
+
.get(sourceEntityId);
|
|
2289
|
+
const target = db
|
|
2290
|
+
.prepare("SELECT canonical_name, kind FROM entities WHERE id = ?")
|
|
2291
|
+
.get(targetEntityId);
|
|
2292
|
+
if (!source || !target || source.kind !== target.kind) return;
|
|
2293
|
+
db.prepare(`
|
|
2294
|
+
INSERT OR IGNORE INTO entity_resolution_suggestions (
|
|
2295
|
+
source_entity_id,
|
|
2296
|
+
target_entity_id,
|
|
2297
|
+
source_name,
|
|
2298
|
+
target_name,
|
|
2299
|
+
kind,
|
|
2300
|
+
observed_alias,
|
|
2301
|
+
normalized_alias
|
|
2302
|
+
)
|
|
2303
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2304
|
+
`).run(
|
|
2305
|
+
sourceEntityId,
|
|
2306
|
+
targetEntityId,
|
|
2307
|
+
source.canonical_name,
|
|
2308
|
+
target.canonical_name,
|
|
2309
|
+
source.kind,
|
|
2310
|
+
normalizeEntityName(observedAlias),
|
|
2311
|
+
normalizeEntityKey(observedAlias),
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
function mergeEntityIntoTarget(
|
|
2316
|
+
sourceEntityId,
|
|
2317
|
+
targetEntityId,
|
|
2318
|
+
suggestionId,
|
|
2319
|
+
resolvedAt,
|
|
2320
|
+
) {
|
|
2321
|
+
const db = getDb();
|
|
2322
|
+
const source = db
|
|
2323
|
+
.prepare("SELECT * FROM entities WHERE id = ?")
|
|
2324
|
+
.get(sourceEntityId);
|
|
2325
|
+
const target = db
|
|
2326
|
+
.prepare("SELECT * FROM entities WHERE id = ?")
|
|
2327
|
+
.get(targetEntityId);
|
|
2328
|
+
if (!source || !target) {
|
|
2329
|
+
throw new Error("Entity resolution merge endpoint no longer exists");
|
|
2330
|
+
}
|
|
2331
|
+
if (source.kind !== target.kind) {
|
|
2332
|
+
throw new Error("Cannot merge entities of different kinds");
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
db.prepare(`
|
|
2336
|
+
INSERT OR IGNORE INTO entity_aliases (
|
|
2337
|
+
entity_id,
|
|
2338
|
+
alias,
|
|
2339
|
+
normalized_alias,
|
|
2340
|
+
canonical,
|
|
2341
|
+
created_at
|
|
2342
|
+
)
|
|
2343
|
+
SELECT ?, alias, normalized_alias, 0, created_at
|
|
2344
|
+
FROM entity_aliases
|
|
2345
|
+
WHERE entity_id = ?
|
|
2346
|
+
`).run(targetEntityId, sourceEntityId);
|
|
2347
|
+
db.prepare(`
|
|
2348
|
+
INSERT OR IGNORE INTO memory_entities (
|
|
2349
|
+
memory_id,
|
|
2350
|
+
entity_id,
|
|
2351
|
+
mention,
|
|
2352
|
+
role,
|
|
2353
|
+
confidence,
|
|
2354
|
+
created_at
|
|
2355
|
+
)
|
|
2356
|
+
SELECT memory_id, ?, mention, role, confidence, created_at
|
|
2357
|
+
FROM memory_entities
|
|
2358
|
+
WHERE entity_id = ?
|
|
2359
|
+
`).run(targetEntityId, sourceEntityId);
|
|
2360
|
+
db.prepare(`
|
|
2361
|
+
INSERT OR IGNORE INTO relationships (
|
|
2362
|
+
source_entity_id,
|
|
2363
|
+
target_entity_id,
|
|
2364
|
+
predicate,
|
|
2365
|
+
memory_id,
|
|
2366
|
+
confidence,
|
|
2367
|
+
evidence,
|
|
2368
|
+
created_at
|
|
2369
|
+
)
|
|
2370
|
+
SELECT
|
|
2371
|
+
CASE WHEN source_entity_id = ? THEN ? ELSE source_entity_id END,
|
|
2372
|
+
CASE WHEN target_entity_id = ? THEN ? ELSE target_entity_id END,
|
|
2373
|
+
predicate,
|
|
2374
|
+
memory_id,
|
|
2375
|
+
confidence,
|
|
2376
|
+
evidence,
|
|
2377
|
+
created_at
|
|
2378
|
+
FROM relationships
|
|
2379
|
+
WHERE source_entity_id = ? OR target_entity_id = ?
|
|
2380
|
+
`).run(
|
|
2381
|
+
sourceEntityId,
|
|
2382
|
+
targetEntityId,
|
|
2383
|
+
sourceEntityId,
|
|
2384
|
+
targetEntityId,
|
|
2385
|
+
sourceEntityId,
|
|
2386
|
+
sourceEntityId,
|
|
2387
|
+
);
|
|
2388
|
+
|
|
2389
|
+
db.prepare("DELETE FROM memory_entities WHERE entity_id = ?")
|
|
2390
|
+
.run(sourceEntityId);
|
|
2391
|
+
db.prepare(`
|
|
2392
|
+
DELETE FROM relationships
|
|
2393
|
+
WHERE source_entity_id = ? OR target_entity_id = ?
|
|
2394
|
+
`).run(sourceEntityId, sourceEntityId);
|
|
2395
|
+
db.prepare(`
|
|
2396
|
+
UPDATE entity_resolution_suggestions
|
|
2397
|
+
SET status = 'rejected', resolved_at = ?
|
|
2398
|
+
WHERE status = 'pending'
|
|
2399
|
+
AND id <> ?
|
|
2400
|
+
AND (
|
|
2401
|
+
source_entity_id = ?
|
|
2402
|
+
OR target_entity_id = ?
|
|
2403
|
+
)
|
|
2404
|
+
`).run(resolvedAt, suggestionId, sourceEntityId, sourceEntityId);
|
|
2405
|
+
db.prepare("DELETE FROM entities WHERE id = ?").run(sourceEntityId);
|
|
2406
|
+
db.prepare(`
|
|
2407
|
+
UPDATE entity_resolution_suggestions
|
|
2408
|
+
SET status = 'merged', resolved_at = ?
|
|
2409
|
+
WHERE id = ?
|
|
2410
|
+
`).run(resolvedAt, suggestionId);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// --- Delete ---
|
|
2414
|
+
|
|
2415
|
+
export function deleteAllMemories() {
|
|
2416
|
+
const db = getDb();
|
|
2417
|
+
db.exec("DELETE FROM memories");
|
|
2418
|
+
db.exec("DELETE FROM memories_fts");
|
|
2419
|
+
db.exec("INSERT INTO memories_fts(memories_fts) VALUES('optimize')");
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
export function deleteAllEntities() {
|
|
2423
|
+
const db = getDb();
|
|
2424
|
+
const cleanup = db.transaction(() => {
|
|
2425
|
+
db.exec("DELETE FROM memory_entities");
|
|
2426
|
+
db.exec("DELETE FROM relationships");
|
|
2427
|
+
db.exec("DELETE FROM entity_resolution_suggestions");
|
|
2428
|
+
db.exec("DELETE FROM entity_aliases");
|
|
2429
|
+
db.exec("DELETE FROM entities");
|
|
2430
|
+
});
|
|
2431
|
+
cleanup();
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
export function deleteMemory(id) {
|
|
2435
|
+
const db = getDb();
|
|
2436
|
+
db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
2437
|
+
removeFtsForMemory(id);
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// --- Graph Data ---
|
|
2441
|
+
|
|
2442
|
+
export function getGraphData() {
|
|
2443
|
+
const db = getDb();
|
|
2444
|
+
|
|
2445
|
+
const memoryRows = db
|
|
2446
|
+
.prepare(
|
|
2447
|
+
`SELECT m.id, m.raw_text, m.summary, m.source, m.created_at,
|
|
2448
|
+
e.extraction_json
|
|
2449
|
+
FROM memories m
|
|
2450
|
+
LEFT JOIN memory_extractions e ON e.id = (
|
|
2451
|
+
SELECT latest.id
|
|
2452
|
+
FROM memory_extractions latest
|
|
2453
|
+
WHERE latest.memory_id = m.id
|
|
2454
|
+
ORDER BY latest.created_at DESC, latest.id DESC
|
|
2455
|
+
LIMIT 1
|
|
2456
|
+
)
|
|
2457
|
+
ORDER BY m.created_at DESC`
|
|
2458
|
+
)
|
|
2459
|
+
.all();
|
|
2460
|
+
|
|
2461
|
+
const memoryNodes = memoryRows.map((row) => {
|
|
2462
|
+
const extraction = row.extraction_json ? JSON.parse(row.extraction_json) : null;
|
|
2463
|
+
return {
|
|
2464
|
+
id: row.id,
|
|
2465
|
+
type: "memory",
|
|
2466
|
+
label: row.summary || row.raw_text || row.id,
|
|
2467
|
+
text: row.raw_text,
|
|
2468
|
+
source: row.source,
|
|
2469
|
+
salience: extraction?.salience ?? 0.5,
|
|
2470
|
+
types: extraction?.types ?? [],
|
|
2471
|
+
createdAt: row.created_at,
|
|
2472
|
+
};
|
|
2473
|
+
});
|
|
2474
|
+
|
|
2475
|
+
const entityRows = db
|
|
2476
|
+
.prepare(
|
|
2477
|
+
`SELECT e.id, e.canonical_name, e.kind,
|
|
2478
|
+
COUNT(DISTINCT me.memory_id) AS memory_count
|
|
2479
|
+
FROM entities e
|
|
2480
|
+
LEFT JOIN memory_entities me ON me.entity_id = e.id
|
|
2481
|
+
GROUP BY e.id
|
|
2482
|
+
ORDER BY memory_count DESC, e.canonical_name ASC`
|
|
2483
|
+
)
|
|
2484
|
+
.all();
|
|
2485
|
+
|
|
2486
|
+
const entityNodes = entityRows.map((row) => ({
|
|
2487
|
+
id: `entity_${row.id}`,
|
|
2488
|
+
entityId: row.id,
|
|
2489
|
+
type: "entity",
|
|
2490
|
+
label: row.canonical_name,
|
|
2491
|
+
kind: row.kind,
|
|
2492
|
+
memoryCount: row.memory_count,
|
|
2493
|
+
}));
|
|
2494
|
+
|
|
2495
|
+
const memoryEntityEdges = db
|
|
2496
|
+
.prepare(
|
|
2497
|
+
`SELECT me.memory_id, me.entity_id, me.mention, me.confidence
|
|
2498
|
+
FROM memory_entities me`
|
|
2499
|
+
)
|
|
2500
|
+
.all()
|
|
2501
|
+
.map((row) => ({
|
|
2502
|
+
source: row.memory_id,
|
|
2503
|
+
target: `entity_${row.entity_id}`,
|
|
2504
|
+
type: "memory-entity",
|
|
2505
|
+
label: row.mention,
|
|
2506
|
+
confidence: row.confidence,
|
|
2507
|
+
}));
|
|
2508
|
+
|
|
2509
|
+
const relationshipEdges = db
|
|
2510
|
+
.prepare(
|
|
2511
|
+
`SELECT r.id, r.source_entity_id, r.target_entity_id,
|
|
2512
|
+
r.predicate, r.confidence, r.memory_id, r.evidence,
|
|
2513
|
+
se.canonical_name AS source_name, se.kind AS source_kind,
|
|
2514
|
+
te.canonical_name AS target_name, te.kind AS target_kind
|
|
2515
|
+
FROM relationships r
|
|
2516
|
+
JOIN entities se ON se.id = r.source_entity_id
|
|
2517
|
+
JOIN entities te ON te.id = r.target_entity_id`
|
|
2518
|
+
)
|
|
2519
|
+
.all()
|
|
2520
|
+
.map((row) => ({
|
|
2521
|
+
source: `entity_${row.source_entity_id}`,
|
|
2522
|
+
target: `entity_${row.target_entity_id}`,
|
|
2523
|
+
type: "relationship",
|
|
2524
|
+
label: row.predicate,
|
|
2525
|
+
confidence: row.confidence,
|
|
2526
|
+
memoryId: row.memory_id,
|
|
2527
|
+
evidence: row.evidence,
|
|
2528
|
+
sourceName: row.source_name,
|
|
2529
|
+
targetName: row.target_name,
|
|
2530
|
+
}));
|
|
2531
|
+
|
|
2532
|
+
return {
|
|
2533
|
+
nodes: [...memoryNodes, ...entityNodes],
|
|
2534
|
+
edges: [...memoryEntityEdges, ...relationshipEdges],
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
export function auditMemoryIntegrity() {
|
|
2539
|
+
const database = getDb();
|
|
2540
|
+
const findings = [];
|
|
2541
|
+
const memories = database.prepare(`
|
|
2542
|
+
SELECT m.*, e.id AS extraction_id, e.extraction_json, e.schema_version
|
|
2543
|
+
FROM memories m
|
|
2544
|
+
LEFT JOIN memory_extractions e ON e.id = (
|
|
2545
|
+
SELECT latest.id FROM memory_extractions latest
|
|
2546
|
+
WHERE latest.memory_id = m.id ORDER BY latest.created_at DESC, latest.id DESC LIMIT 1
|
|
2547
|
+
)
|
|
2548
|
+
ORDER BY m.id
|
|
2549
|
+
`).all();
|
|
2550
|
+
|
|
2551
|
+
const shapesByVersion = new Map();
|
|
2552
|
+
for (const memory of memories) {
|
|
2553
|
+
const extraction = parseJson(memory.extraction_json, null);
|
|
2554
|
+
if (!extraction) {
|
|
2555
|
+
findings.push({ code: "missing_extraction", memoryId: memory.id });
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
const shape = extraction.evidenceSpans && extraction.durability
|
|
2559
|
+
? "semantic_v3_atom"
|
|
2560
|
+
: extraction.memories ? "semantic_collection" : "legacy_atom";
|
|
2561
|
+
const shapes = shapesByVersion.get(memory.schema_version) || new Set();
|
|
2562
|
+
shapes.add(shape);
|
|
2563
|
+
shapesByVersion.set(memory.schema_version, shapes);
|
|
2564
|
+
if (extraction.text && extraction.text !== memory.raw_text) {
|
|
2565
|
+
findings.push({
|
|
2566
|
+
code: "extraction_text_mismatch",
|
|
2567
|
+
memoryId: memory.id,
|
|
2568
|
+
extractionId: memory.extraction_id,
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
if (likelyMultiFact(memory.raw_text, extraction)) {
|
|
2572
|
+
findings.push({ code: "likely_multi_fact_atom", memoryId: memory.id });
|
|
2573
|
+
}
|
|
2574
|
+
if (memory.summary && hasNegationMismatch(memory.raw_text, memory.summary)) {
|
|
2575
|
+
findings.push({ code: "possible_summary_contradiction", memoryId: memory.id });
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
for (const [schemaVersion, shapes] of shapesByVersion) {
|
|
2580
|
+
if (shapes.size > 1) {
|
|
2581
|
+
findings.push({
|
|
2582
|
+
code: "incompatible_schema_shapes",
|
|
2583
|
+
schemaVersion,
|
|
2584
|
+
shapes: [...shapes].sort(),
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
const unsupportedRelationships = database.prepare(`
|
|
2590
|
+
SELECT r.id, r.memory_id, r.evidence, m.raw_text,
|
|
2591
|
+
GROUP_CONCAT(s.text, char(0)) AS source_texts
|
|
2592
|
+
FROM relationships r
|
|
2593
|
+
JOIN memories m ON m.id = r.memory_id
|
|
2594
|
+
LEFT JOIN source_memory_links l ON l.memory_id = m.id
|
|
2595
|
+
LEFT JOIN memory_sources s ON s.id = l.source_id
|
|
2596
|
+
GROUP BY r.id
|
|
2597
|
+
`).all().filter((row) => row.evidence
|
|
2598
|
+
&& !row.raw_text.includes(row.evidence)
|
|
2599
|
+
&& !String(row.source_texts || "").split("\u0000").some(
|
|
2600
|
+
(sourceText) => sourceText.includes(row.evidence),
|
|
2601
|
+
));
|
|
2602
|
+
for (const relationship of unsupportedRelationships) {
|
|
2603
|
+
findings.push({
|
|
2604
|
+
code: "relationship_evidence_missing",
|
|
2605
|
+
memoryId: relationship.memory_id,
|
|
2606
|
+
relationshipId: relationship.id,
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
for (const row of database.prepare(`
|
|
2611
|
+
SELECT s.id FROM memory_sources s
|
|
2612
|
+
LEFT JOIN source_memory_links l ON l.source_id = s.id
|
|
2613
|
+
WHERE s.extraction_status = 'completed'
|
|
2614
|
+
GROUP BY s.id HAVING COUNT(l.id) = 0
|
|
2615
|
+
`).all()) {
|
|
2616
|
+
findings.push({ code: "orphaned_source", sourceId: row.id });
|
|
2617
|
+
}
|
|
2618
|
+
for (const row of database.prepare(`
|
|
2619
|
+
SELECT a.id, a.memory_id FROM cognitive_annotations a
|
|
2620
|
+
LEFT JOIN memories m ON m.id = a.memory_id WHERE m.id IS NULL
|
|
2621
|
+
`).all()) {
|
|
2622
|
+
findings.push({
|
|
2623
|
+
code: "orphaned_annotation",
|
|
2624
|
+
annotationId: row.id,
|
|
2625
|
+
memoryId: row.memory_id,
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
for (const row of database.prepare(`
|
|
2629
|
+
SELECT m.id, m.version,
|
|
2630
|
+
MAX(CASE WHEN v.status = 'completed' THEN v.memory_version END) AS indexed_version
|
|
2631
|
+
FROM memories m LEFT JOIN vector_index_jobs v ON v.memory_id = m.id
|
|
2632
|
+
GROUP BY m.id
|
|
2633
|
+
HAVING indexed_version IS NULL OR indexed_version < m.version
|
|
2634
|
+
`).all()) {
|
|
2635
|
+
findings.push({
|
|
2636
|
+
code: "missing_or_stale_vector_index",
|
|
2637
|
+
memoryId: row.id,
|
|
2638
|
+
memoryVersion: row.version,
|
|
2639
|
+
indexedVersion: row.indexed_version,
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
const counts = findings.reduce((result, finding) => {
|
|
2644
|
+
result[finding.code] = (result[finding.code] || 0) + 1;
|
|
2645
|
+
return result;
|
|
2646
|
+
}, {});
|
|
2647
|
+
return {
|
|
2648
|
+
generatedAt: new Date().toISOString(),
|
|
2649
|
+
memoryCount: memories.length,
|
|
2650
|
+
findingCount: findings.length,
|
|
2651
|
+
counts,
|
|
2652
|
+
findings,
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
function likelyMultiFact(text, extraction) {
|
|
2657
|
+
if ((String(text).match(/[.!?]+(?:\s+|$)/g) || []).length > 1) return true;
|
|
2658
|
+
const subjects = new Set((extraction.relationships || [])
|
|
2659
|
+
.map((relationship) => String(relationship.subject).toLocaleLowerCase()));
|
|
2660
|
+
return subjects.size > 1 || (/\band\b/i.test(text)
|
|
2661
|
+
&& (extraction.relationships || []).length > 1);
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
function hasNegationMismatch(text, summary) {
|
|
2665
|
+
const negation = (value) => /\b(?:not|never|no longer|doesn't|don't|isn't|wasn't)\b/i.test(value);
|
|
2666
|
+
return negation(text) !== negation(summary);
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
export function closeDb() {
|
|
2670
|
+
if (db) {
|
|
2671
|
+
db.close();
|
|
2672
|
+
db = null;
|
|
2673
|
+
}
|
|
2674
|
+
}
|