@equationalapplications/expo-llm-wiki 2.2.0 → 2.4.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/dist/index.js CHANGED
@@ -15,1117 +15,47 @@ var __copyProps = (to, from, except, desc) => {
15
15
  }
16
16
  return to;
17
17
  };
18
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
18
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
20
 
20
21
  // src/index.ts
21
22
  var index_exports = {};
22
23
  __export(index_exports, {
23
- WikiBusyError: () => WikiBusyError,
24
- WikiMemory: () => WikiMemory,
25
- createWiki: () => createWiki,
26
- formatMemoryDump: () => formatMemoryDump
24
+ createWiki: () => createWiki
27
25
  });
28
26
  module.exports = __toCommonJS(index_exports);
27
+ var import_core_llm_wiki = require("@equationalapplications/core-llm-wiki");
29
28
 
30
- // src/db/schema.ts
31
- async function setupDatabase(db, prefix) {
32
- await db.execAsync(`
33
- CREATE TABLE IF NOT EXISTS ${prefix}entries (
34
- id TEXT PRIMARY KEY,
35
- entity_id TEXT NOT NULL,
36
- title TEXT NOT NULL,
37
- body TEXT NOT NULL,
38
- tags TEXT NOT NULL DEFAULT '[]',
39
- confidence TEXT NOT NULL DEFAULT 'inferred',
40
- source_type TEXT NOT NULL DEFAULT 'agent_inferred',
41
- source_hash TEXT,
42
- source_ref TEXT,
43
- created_at INTEGER NOT NULL,
44
- updated_at INTEGER NOT NULL,
45
- last_accessed_at INTEGER,
46
- access_count INTEGER NOT NULL DEFAULT 0,
47
- deleted_at INTEGER
48
- );
49
-
50
- CREATE INDEX IF NOT EXISTS ${prefix}entries_entity_idx ON ${prefix}entries(entity_id);
51
- CREATE INDEX IF NOT EXISTS ${prefix}entries_source_ref_idx ON ${prefix}entries(entity_id, source_ref);
52
- CREATE INDEX IF NOT EXISTS ${prefix}entries_source_hash_idx ON ${prefix}entries(entity_id, source_hash) WHERE source_hash IS NOT NULL;
53
- CREATE INDEX IF NOT EXISTS ${prefix}entries_updated_idx ON ${prefix}entries(updated_at DESC);
54
-
55
- -- FTS5 Virtual Table for full-text search
56
- CREATE VIRTUAL TABLE IF NOT EXISTS ${prefix}entries_fts USING fts5(
57
- title,
58
- body,
59
- tags,
60
- content='${prefix}entries',
61
- content_rowid='rowid',
62
- tokenize='porter unicode61'
63
- );
64
-
65
- -- Triggers to keep FTS5 in sync with entries
66
- CREATE TRIGGER IF NOT EXISTS ${prefix}entries_ai AFTER INSERT ON ${prefix}entries BEGIN
67
- INSERT INTO ${prefix}entries_fts(rowid, title, body, tags)
68
- VALUES (new.rowid, new.title, new.body, new.tags);
69
- END;
70
-
71
- CREATE TRIGGER IF NOT EXISTS ${prefix}entries_ad AFTER DELETE ON ${prefix}entries BEGIN
72
- INSERT INTO ${prefix}entries_fts(${prefix}entries_fts, rowid, title, body, tags)
73
- VALUES ('delete', old.rowid, old.title, old.body, old.tags);
74
- END;
75
-
76
- CREATE TRIGGER IF NOT EXISTS ${prefix}entries_au AFTER UPDATE ON ${prefix}entries BEGIN
77
- INSERT INTO ${prefix}entries_fts(${prefix}entries_fts, rowid, title, body, tags)
78
- VALUES ('delete', old.rowid, old.title, old.body, old.tags);
79
- INSERT INTO ${prefix}entries_fts(rowid, title, body, tags)
80
- VALUES (new.rowid, new.title, new.body, new.tags);
81
- END;
82
-
83
- CREATE TABLE IF NOT EXISTS ${prefix}tasks (
84
- id TEXT PRIMARY KEY,
85
- entity_id TEXT NOT NULL,
86
- description TEXT NOT NULL,
87
- status TEXT NOT NULL DEFAULT 'pending',
88
- priority INTEGER NOT NULL DEFAULT 0,
89
- created_at INTEGER NOT NULL,
90
- updated_at INTEGER NOT NULL,
91
- resolved_at INTEGER,
92
- deleted_at INTEGER
93
- );
94
-
95
- CREATE INDEX IF NOT EXISTS ${prefix}tasks_entity_idx ON ${prefix}tasks(entity_id, status);
96
-
97
- CREATE TABLE IF NOT EXISTS ${prefix}events (
98
- id TEXT PRIMARY KEY,
99
- entity_id TEXT NOT NULL,
100
- event_type TEXT NOT NULL,
101
- summary TEXT NOT NULL,
102
- related_entry_id TEXT,
103
- created_at INTEGER NOT NULL
104
- );
105
-
106
- CREATE INDEX IF NOT EXISTS ${prefix}events_entity_idx ON ${prefix}events(entity_id, created_at DESC);
107
-
108
- CREATE TABLE IF NOT EXISTS ${prefix}checkpoints (
109
- entity_id TEXT PRIMARY KEY,
110
- heal_checkpoint INTEGER NOT NULL DEFAULT 0,
111
- memory_checkpoint INTEGER NOT NULL DEFAULT 0
112
- );
113
- `);
114
- }
115
-
116
- // src/types.ts
117
- var WikiBusyError = class extends Error {
118
- operation;
119
- entityId;
120
- constructor(operation, entityId) {
121
- super(`${operation} already running for entity ${entityId}`);
122
- this.name = "WikiBusyError";
123
- this.operation = operation;
124
- this.entityId = entityId;
125
- }
126
- };
127
-
128
- // src/prompts.ts
129
- var LIBRARIAN_SYSTEM_PROMPT = `You are a knowledge extraction agent. Your job is to analyze recent episodic events and extract stable facts and actionable tasks about the user or entity.
130
- Return ONLY a valid JSON object matching this schema:
131
- {
132
- "facts": [{ "title": "string (max 80 chars)", "body": "string (max 800 chars)", "tags": ["string"], "confidence": "certain|inferred|tentative" }],
133
- "tasks": [{ "description": "string", "priority": "number (0-10)" }]
134
- }
135
- Keep facts concise. Do not return markdown, just raw JSON.`;
136
- var HEAL_SYSTEM_PROMPT = `You are a memory grooming agent. Your job is to review a full dump of facts and recent events to resolve contradictions, downgrade stale claims, and flag obsolete facts for deletion.
137
- Return ONLY a valid JSON object matching this schema:
138
- {
139
- "downgraded": ["string (fact IDs)"],
140
- "deleted": ["string (fact IDs)"],
141
- "newFacts": [{ "title": "string (max 80 chars)", "body": "string (max 800 chars)", "tags": ["string"], "confidence": "certain|inferred|tentative" }]
142
- }
143
- Do not return markdown, just raw JSON.`;
144
- var INGEST_SYSTEM_PROMPT = `You are a document ingestion agent. Your job is to extract factual knowledge from the provided document chunk.
145
- Return ONLY a valid JSON object matching this schema:
146
- {
147
- "facts": [{ "title": "string (max 80 chars)", "body": "string (max 800 chars)", "tags": ["string"], "confidence": "certain|inferred|tentative" }]
148
- }
149
- Extract verbatim factual content. Do not return markdown, just raw JSON.`;
150
-
151
- // src/WikiMemory.ts
152
- function parseJsonResponse(text) {
153
- const firstBrace = text.indexOf("{");
154
- const firstBracket = text.indexOf("[");
155
- let start;
156
- let openChar;
157
- let closeChar;
158
- if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {
159
- start = firstBrace;
160
- openChar = "{";
161
- closeChar = "}";
162
- } else if (firstBracket !== -1) {
163
- start = firstBracket;
164
- openChar = "[";
165
- closeChar = "]";
166
- } else {
167
- throw new SyntaxError("No JSON object/array found in LLM response");
168
- }
169
- let depth = 0;
170
- let inString = false;
171
- let escape = false;
172
- let end = -1;
173
- for (let i = start; i < text.length; i++) {
174
- const ch = text[i];
175
- if (escape) {
176
- escape = false;
177
- continue;
178
- }
179
- if (ch === "\\" && inString) {
180
- escape = true;
181
- continue;
182
- }
183
- if (ch === '"') {
184
- inString = !inString;
185
- continue;
186
- }
187
- if (inString) continue;
188
- if (ch === openChar) {
189
- depth++;
190
- continue;
191
- }
192
- if (ch === closeChar) {
193
- depth--;
194
- if (depth === 0) {
195
- end = i;
196
- break;
197
- }
198
- }
199
- }
200
- if (end === -1) throw new SyntaxError("No JSON object/array found in LLM response");
201
- return JSON.parse(text.slice(start, end + 1));
202
- }
203
- function generateId(prefix = "") {
204
- return prefix + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
205
- }
206
- function safeSlice(value, start, end) {
207
- const length = value.length;
208
- let safeStart = start < 0 ? Math.max(length + start, 0) : Math.min(start, length);
209
- let safeEnd = end === void 0 ? length : end < 0 ? Math.max(length + end, 0) : Math.min(end, length);
210
- if (safeStart > safeEnd) {
211
- [safeStart, safeEnd] = [safeEnd, safeStart];
212
- }
213
- if (safeStart > 0 && safeStart < length && value.charCodeAt(safeStart) >= 56320 && value.charCodeAt(safeStart) <= 57343 && value.charCodeAt(safeStart - 1) >= 55296 && value.charCodeAt(safeStart - 1) <= 56319) {
214
- safeStart--;
215
- }
216
- if (safeEnd > 0 && safeEnd < length && value.charCodeAt(safeEnd - 1) >= 55296 && value.charCodeAt(safeEnd - 1) <= 56319 && value.charCodeAt(safeEnd) >= 56320 && value.charCodeAt(safeEnd) <= 57343) {
217
- safeEnd--;
218
- }
219
- return value.slice(safeStart, safeEnd);
220
- }
221
- function chunkText(input, maxChunkLength, overlap) {
222
- const text = input.trim();
223
- if (text.length === 0) return { chunks: [], truncated: false };
224
- if (!Number.isInteger(maxChunkLength) || maxChunkLength < 2) {
225
- throw new Error("maxChunkLength must be an integer >= 2");
226
- }
227
- if (!Number.isInteger(overlap) || overlap < 0 || overlap >= maxChunkLength) {
228
- throw new Error("overlap must be a non-negative integer < maxChunkLength");
229
- }
230
- const chunks = [];
231
- let truncated = false;
232
- let cursor = 0;
233
- const halfMax = Math.floor(maxChunkLength / 2);
234
- while (cursor < text.length) {
235
- const remaining = text.length - cursor;
236
- if (remaining <= maxChunkLength) {
237
- chunks.push(safeSlice(text, cursor, text.length));
238
- break;
239
- }
240
- const windowEnd = cursor + maxChunkLength;
241
- const minSplit = cursor + halfMax;
242
- let splitPoint = -1;
243
- const paraIdx = text.lastIndexOf("\n\n", windowEnd);
244
- if (paraIdx >= minSplit && paraIdx + 2 <= windowEnd) {
245
- splitPoint = paraIdx + 2;
246
- }
247
- if (splitPoint === -1) {
248
- let lastTerm = -1;
249
- for (let i = minSplit; i < windowEnd - 1; i++) {
250
- const ch = text[i];
251
- if ((ch === "." || ch === "!" || ch === "?") && /\s/.test(text[i + 1])) {
252
- lastTerm = i + 2;
253
- }
254
- }
255
- if (lastTerm !== -1 && lastTerm <= windowEnd) splitPoint = lastTerm;
256
- }
257
- if (splitPoint === -1) {
258
- for (let i = windowEnd - 1; i >= minSplit; i--) {
259
- if (/\s/.test(text[i])) {
260
- splitPoint = i + 1;
261
- break;
262
- }
263
- }
264
- }
265
- if (splitPoint === -1) {
266
- truncated = true;
267
- splitPoint = windowEnd;
268
- }
269
- chunks.push(safeSlice(text, cursor, splitPoint));
270
- const next = Math.max(splitPoint - overlap, cursor + 1);
271
- cursor = next;
272
- }
273
- return { chunks, truncated };
274
- }
275
- async function withConcurrency(tasks, limit) {
276
- const results = new Array(tasks.length);
277
- let index = 0;
278
- let failed = false;
279
- let firstError;
280
- async function worker() {
281
- while (index < tasks.length && !failed) {
282
- const i = index++;
283
- try {
284
- results[i] = await tasks[i]();
285
- } catch (e) {
286
- if (!failed) {
287
- failed = true;
288
- firstError = e;
289
- }
290
- return;
291
- }
292
- }
293
- }
294
- const workerCount = tasks.length === 0 ? 0 : Math.min(Math.max(limit, 1), tasks.length);
295
- await Promise.allSettled(Array.from({ length: workerCount }, worker));
296
- if (failed) throw firstError;
297
- return results;
298
- }
299
- function clip(value, max) {
300
- if (typeof value !== "string") return "";
301
- const s = value.trim();
302
- return s.length <= max ? s : safeSlice(s, 0, max).trimEnd();
303
- }
304
- function validateTags(tags) {
305
- if (!Array.isArray(tags)) return [];
306
- return tags.filter((t) => typeof t === "string").map((t) => t.trim().toLowerCase()).filter((t) => t.length > 0 && t.length <= 40).slice(0, 6);
307
- }
308
- function validateFact(fact) {
309
- if (typeof fact?.title !== "string" || typeof fact?.body !== "string") return null;
310
- const title = clip(fact.title, 80);
311
- const body = clip(fact.body, 800);
312
- if (!title || !body) return null;
313
- let confidence = fact.confidence;
314
- if (confidence !== "certain" && confidence !== "tentative") confidence = "inferred";
315
- return {
316
- ...fact,
317
- title,
318
- body,
319
- confidence,
320
- tags: validateTags(fact.tags)
321
- };
322
- }
323
- function validateTask(task) {
324
- if (typeof task?.description !== "string") return null;
325
- const description = clip(task.description, 200);
326
- if (!description) return null;
327
- let priority = task.priority;
328
- if (typeof priority !== "number" || !isFinite(priority)) priority = 0;
329
- return {
330
- ...task,
331
- description,
332
- priority
333
- };
334
- }
335
- function normalizeSourceRef(value) {
336
- if (typeof value !== "string") return null;
337
- const cleaned = value.replace(/[^A-Za-z0-9._\- ]/g, "").trim().slice(0, 255);
338
- return cleaned.length > 0 ? cleaned : null;
339
- }
340
- function normalizeSourceHash(value) {
341
- if (typeof value !== "string") return null;
342
- return /^[0-9a-f]{64}$/i.test(value) ? value.toLowerCase() : null;
343
- }
344
- function titleTokens(title) {
345
- return new Set(title.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((t) => t.length >= 3));
346
- }
347
- function jaccardScore(a, b) {
348
- if (a.size === 0 && b.size === 0) return 0;
349
- const intersection = new Set([...a].filter((x) => b.has(x)));
350
- const union = /* @__PURE__ */ new Set([...a, ...b]);
351
- return intersection.size / union.size;
352
- }
353
- var FUZZY_THRESHOLD = 0.5;
354
- var MIN_TOKENS_TO_QUALIFY = 3;
355
- var WikiMemory = class {
356
- db;
357
- prefix;
358
- options;
359
- activeMaintenanceJobs = /* @__PURE__ */ new Set();
360
- activeIngestJobs = /* @__PURE__ */ new Set();
361
- _librarianKey(entityId) {
362
- return `${this.prefix}:${entityId}:librarian`;
363
- }
364
- _healKey(entityId) {
365
- return `${this.prefix}:${entityId}:heal`;
366
- }
367
- _warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
368
- console.warn(`[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`);
369
- }
370
- constructor(db, options) {
371
- this.db = db;
372
- this.options = options;
373
- this.prefix = options.config?.tablePrefix || "llm_wiki_";
374
- }
375
- async setup() {
376
- await setupDatabase(this.db, this.prefix);
377
- const ftsMeta = await this.db.getFirstAsync(
378
- `SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
379
- [`${this.prefix}entries_fts`]
380
- );
381
- const hasPorterTokenizer = /tokenize\s*=\s*['"]porter\s+unicode61['"]/i.test(ftsMeta?.sql ?? "");
382
- if (ftsMeta?.sql && !hasPorterTokenizer) {
383
- await this.db.withTransactionAsync(async () => {
384
- await this.db.execAsync(`
385
- DROP TRIGGER IF EXISTS ${this.prefix}entries_ai;
386
- DROP TRIGGER IF EXISTS ${this.prefix}entries_ad;
387
- DROP TRIGGER IF EXISTS ${this.prefix}entries_au;
388
- DROP TABLE IF EXISTS ${this.prefix}entries_fts;
389
- CREATE VIRTUAL TABLE ${this.prefix}entries_fts USING fts5(
390
- title,
391
- body,
392
- tags,
393
- content='${this.prefix}entries',
394
- content_rowid='rowid',
395
- tokenize='porter unicode61'
396
- );
397
- INSERT INTO ${this.prefix}entries_fts(rowid, title, body, tags)
398
- SELECT rowid, title, body, tags FROM ${this.prefix}entries;
399
- CREATE TRIGGER ${this.prefix}entries_ai AFTER INSERT ON ${this.prefix}entries BEGIN
400
- INSERT INTO ${this.prefix}entries_fts(rowid, title, body, tags)
401
- VALUES (new.rowid, new.title, new.body, new.tags);
402
- END;
403
- CREATE TRIGGER ${this.prefix}entries_ad AFTER DELETE ON ${this.prefix}entries BEGIN
404
- INSERT INTO ${this.prefix}entries_fts(${this.prefix}entries_fts, rowid, title, body, tags)
405
- VALUES ('delete', old.rowid, old.title, old.body, old.tags);
406
- END;
407
- CREATE TRIGGER ${this.prefix}entries_au AFTER UPDATE ON ${this.prefix}entries BEGIN
408
- INSERT INTO ${this.prefix}entries_fts(${this.prefix}entries_fts, rowid, title, body, tags)
409
- VALUES ('delete', old.rowid, old.title, old.body, old.tags);
410
- INSERT INTO ${this.prefix}entries_fts(rowid, title, body, tags)
411
- VALUES (new.rowid, new.title, new.body, new.tags);
412
- END;
413
- `);
414
- });
415
- }
416
- const rows = await this.db.getAllAsync(`
417
- SELECT rowid, source_ref FROM ${this.prefix}entries
418
- WHERE source_ref IS NOT NULL
419
- AND (
420
- TRIM(source_ref) != source_ref
421
- OR INSTR(source_ref, '/') > 0
422
- OR INSTR(source_ref, '\\') > 0
423
- OR INSTR(source_ref, CHAR(0)) > 0
424
- OR source_ref GLOB '*[^-A-Za-z0-9._ ]*'
425
- )
426
- `);
427
- await this.db.withTransactionAsync(async () => {
428
- for (const row of rows) {
429
- const normalized = normalizeSourceRef(row.source_ref);
430
- if (normalized !== row.source_ref) {
431
- await this.db.runAsync(
432
- `UPDATE ${this.prefix}entries SET source_ref = ? WHERE rowid = ?`,
433
- [normalized, row.rowid]
434
- );
435
- }
436
- }
437
- });
438
- }
439
- formatSearchQuery(query) {
440
- const normalizeTokens = (value) => value.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((t) => t.length >= 3);
441
- const baseTokens = normalizeTokens(query);
442
- if (baseTokens.length === 0) return "";
443
- const synonymMap = this.options.config?.synonymMap;
444
- const expanded = [];
445
- const seen = /* @__PURE__ */ new Set();
446
- const pushNormalized = (value) => {
447
- for (const token of normalizeTokens(value)) {
448
- if (expanded.length >= 12) return false;
449
- if (seen.has(token)) continue;
450
- seen.add(token);
451
- expanded.push(token);
452
- }
453
- return true;
454
- };
455
- for (const t of baseTokens) {
456
- if (!pushNormalized(t)) break;
457
- if (synonymMap) {
458
- const synonyms = synonymMap[t];
459
- if (Array.isArray(synonyms)) {
460
- for (const s of synonyms) {
461
- if (typeof s === "string") {
462
- if (!pushNormalized(s)) break;
463
- }
464
- }
465
- }
466
- }
467
- }
468
- return expanded.map((t) => `"${t}"*`).join(" OR ");
469
- }
470
- async read(entityId, query) {
471
- const ftsQuery = this.formatSearchQuery(query);
472
- const maxResults = this.options.config?.maxFtsResults || 10;
473
- let factsPromise;
474
- if (ftsQuery) {
475
- factsPromise = this.db.getAllAsync(`
476
- SELECT e.* FROM ${this.prefix}entries e
477
- JOIN ${this.prefix}entries_fts fts ON e.rowid = fts.rowid
478
- WHERE fts.${this.prefix}entries_fts MATCH ?
479
- AND e.entity_id = ?
480
- AND e.deleted_at IS NULL
481
- ORDER BY e.confidence DESC, e.access_count DESC, e.updated_at DESC
482
- LIMIT ?
483
- `, [ftsQuery, entityId, maxResults]);
484
- } else {
485
- factsPromise = this.db.getAllAsync(`
486
- SELECT * FROM ${this.prefix}entries
487
- WHERE entity_id = ? AND deleted_at IS NULL
488
- ORDER BY updated_at DESC
489
- LIMIT ?
490
- `, [entityId, maxResults]);
491
- }
492
- const tasksPromise = this.db.getAllAsync(`
493
- SELECT * FROM ${this.prefix}tasks
494
- WHERE entity_id = ? AND status IN ('pending', 'in_progress') AND deleted_at IS NULL
495
- ORDER BY priority DESC, created_at ASC
496
- `, [entityId]);
497
- const eventsPromise = this.db.getAllAsync(`
498
- SELECT * FROM ${this.prefix}events
499
- WHERE entity_id = ?
500
- ORDER BY created_at DESC
501
- LIMIT 10
502
- `, [entityId]);
503
- const [factsRaw, tasks, events] = await Promise.all([factsPromise, tasksPromise, eventsPromise]);
504
- if (ftsQuery && factsRaw.length > 0) {
505
- const ids = factsRaw.map((f) => f.id);
506
- const placeholders = ids.map(() => "?").join(",");
507
- const now = Date.now();
508
- await this.db.runAsync(`
509
- UPDATE ${this.prefix}entries
510
- SET access_count = access_count + 1, last_accessed_at = ?
511
- WHERE id IN (${placeholders})
512
- `, [now, ...ids]);
513
- }
514
- const facts = factsRaw.map((f) => ({
515
- ...f,
516
- tags: typeof f.tags === "string" ? JSON.parse(f.tags) : f.tags
517
- }));
518
- return { facts, tasks, events: events.reverse() };
519
- }
520
- async getMemoryBundle(entityId) {
521
- return this._getFullBundle(entityId, { maxEvents: 10 });
522
- }
523
- async write(entityId, event) {
524
- const id = generateId("evt_");
525
- const now = Date.now();
526
- let eventType = event.event_type;
527
- if (!["observation", "decision", "action", "outcome"].includes(eventType)) {
528
- eventType = "observation";
529
- }
530
- await this.db.runAsync(`
531
- INSERT INTO ${this.prefix}events (id, entity_id, event_type, summary, related_entry_id, created_at)
532
- VALUES (?, ?, ?, ?, ?, ?)
533
- `, [id, entityId, eventType, event.summary, event.related_entry_id || null, now]);
534
- const threshold = this.options.config?.autoLibrarianThreshold || 20;
535
- const [row, cp] = await Promise.all([
536
- this.db.getFirstAsync(`SELECT COUNT(*) as count FROM ${this.prefix}events WHERE entity_id = ?`, [entityId]),
537
- this.db.getFirstAsync(`SELECT * FROM ${this.prefix}checkpoints WHERE entity_id = ?`, [entityId])
538
- ]);
539
- const count = row?.count || 0;
540
- let memoryCheckpoint = cp?.memory_checkpoint || 0;
541
- if (memoryCheckpoint > count) memoryCheckpoint = 0;
542
- if (count - memoryCheckpoint >= threshold) {
543
- const jobKey = this._librarianKey(entityId);
544
- if (!this.activeMaintenanceJobs.has(jobKey)) {
545
- this.activeMaintenanceJobs.add(jobKey);
546
- this.runLibrarianThenMaybeHeal(entityId, count).catch(console.error).finally(() => this.activeMaintenanceJobs.delete(jobKey));
547
- }
548
- }
549
- }
550
- async runLibrarianThenMaybeHeal(entityId, currentEventCount) {
551
- await this._doRunLibrarian(entityId);
552
- await this.db.runAsync(`
553
- INSERT INTO ${this.prefix}checkpoints (entity_id, memory_checkpoint)
554
- VALUES (?, ?)
555
- ON CONFLICT(entity_id) DO UPDATE SET memory_checkpoint = ?
556
- `, [entityId, currentEventCount, currentEventCount]);
557
- const autoHealThreshold = this.options.config?.autoHealThreshold || 100;
558
- const cp = await this.db.getFirstAsync(`SELECT * FROM ${this.prefix}checkpoints WHERE entity_id = ?`, [entityId]);
559
- let healCheckpoint = cp?.heal_checkpoint || 0;
560
- if (healCheckpoint > currentEventCount) healCheckpoint = 0;
561
- if (currentEventCount - healCheckpoint >= autoHealThreshold) {
562
- const healKey = this._healKey(entityId);
563
- if (!this.activeMaintenanceJobs.has(healKey)) {
564
- this.activeMaintenanceJobs.add(healKey);
565
- try {
566
- await this._doRunHeal(entityId);
567
- await this.db.runAsync(`
568
- INSERT INTO ${this.prefix}checkpoints (entity_id, heal_checkpoint)
569
- VALUES (?, ?)
570
- ON CONFLICT(entity_id) DO UPDATE SET heal_checkpoint = ?
571
- `, [entityId, currentEventCount, currentEventCount]);
572
- } finally {
573
- this.activeMaintenanceJobs.delete(healKey);
574
- }
575
- }
576
- }
577
- }
578
- async _doRunLibrarian(entityId) {
579
- const events = await this.db.getAllAsync(`
580
- SELECT * FROM ${this.prefix}events
581
- WHERE entity_id = ?
582
- ORDER BY created_at DESC
583
- LIMIT 50
584
- `, [entityId]);
585
- const currentFactsRows = await this.db.getAllAsync(`
586
- SELECT * FROM ${this.prefix}entries
587
- WHERE entity_id = ? AND deleted_at IS NULL
588
- ORDER BY updated_at DESC
589
- LIMIT 100
590
- `, [entityId]);
591
- const currentFacts = currentFactsRows.map((f) => ({
592
- ...f,
593
- tags: typeof f.tags === "string" ? JSON.parse(f.tags) : f.tags
594
- }));
595
- const userPrompt = `Events:
596
- ${JSON.stringify(events.reverse(), null, 2)}
597
-
598
- Current Facts:
599
- ${JSON.stringify(currentFacts, null, 2)}`;
600
- const responseText = await this.options.llmProvider.generateText({
601
- systemPrompt: LIBRARIAN_SYSTEM_PROMPT,
602
- userPrompt
603
- });
604
- const result = parseJsonResponse(responseText);
605
- const facts = Array.isArray(result.facts) ? result.facts : [];
606
- const tasks = Array.isArray(result.tasks) ? result.tasks : [];
607
- const validFacts = facts.map(validateFact).filter((f) => f !== null);
608
- const validTasks = tasks.map(validateTask).filter((t) => t !== null);
609
- const now = Date.now();
610
- await this.db.withTransactionAsync(async () => {
611
- for (const fact of validFacts) {
612
- const newTokens = titleTokens(fact.title);
613
- let skip = false;
614
- if (newTokens.size >= MIN_TOKENS_TO_QUALIFY) {
615
- for (const existing of currentFactsRows) {
616
- if (existing.source_type !== "agent_inferred") continue;
617
- const existingTokens = titleTokens(existing.title);
618
- if (existingTokens.size >= MIN_TOKENS_TO_QUALIFY) {
619
- if (jaccardScore(newTokens, existingTokens) >= FUZZY_THRESHOLD) {
620
- skip = true;
621
- break;
622
- }
623
- }
624
- }
625
- }
626
- if (skip) continue;
627
- const id = generateId("fact_");
628
- await this.db.runAsync(`
629
- INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
630
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
631
- `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "agent_inferred", now, now]);
632
- }
633
- for (const task of validTasks) {
634
- const id = generateId("task_");
635
- await this.db.runAsync(`
636
- INSERT INTO ${this.prefix}tasks (id, entity_id, description, status, priority, created_at, updated_at)
637
- VALUES (?, ?, ?, ?, ?, ?, ?)
638
- `, [id, entityId, task.description, "pending", task.priority, now, now]);
639
- }
640
- });
641
- }
642
- async _doRunHeal(entityId) {
643
- const now = Date.now();
644
- const orphanAfterDays = this.options.config?.orphanAfterDays !== void 0 ? this.options.config.orphanAfterDays : 30;
645
- const staleInferredAfterDays = this.options.config?.staleInferredAfterDays !== void 0 ? this.options.config.staleInferredAfterDays : 60;
646
- const MS_PER_DAY = 24 * 60 * 60 * 1e3;
647
- if (orphanAfterDays !== null && (typeof orphanAfterDays !== "number" || !Number.isFinite(orphanAfterDays) || orphanAfterDays < 0)) {
648
- throw new Error("Invalid orphanAfterDays: must be a finite number >= 0 or null");
649
- }
650
- if (staleInferredAfterDays !== null && (typeof staleInferredAfterDays !== "number" || !Number.isFinite(staleInferredAfterDays) || staleInferredAfterDays < 0)) {
651
- throw new Error("Invalid staleInferredAfterDays: must be a finite number >= 0 or null");
652
- }
653
- await this.db.withTransactionAsync(async () => {
654
- if (orphanAfterDays !== null) {
655
- const orphanThreshold = now - orphanAfterDays * MS_PER_DAY;
656
- await this.db.runAsync(`
657
- UPDATE ${this.prefix}entries
658
- SET deleted_at = ?, updated_at = ?
659
- WHERE entity_id = ? AND access_count = 0 AND created_at < ? AND source_type != 'user_document' AND deleted_at IS NULL
660
- `, [now, now, entityId, orphanThreshold]);
661
- }
662
- if (staleInferredAfterDays !== null) {
663
- const staleThreshold = now - staleInferredAfterDays * MS_PER_DAY;
664
- await this.db.runAsync(`
665
- UPDATE ${this.prefix}entries
666
- SET confidence = 'tentative', updated_at = ?
667
- WHERE entity_id = ? AND confidence = 'inferred' AND (last_accessed_at < ? OR (last_accessed_at IS NULL AND created_at < ?)) AND source_type != 'user_document' AND deleted_at IS NULL
668
- `, [now, entityId, staleThreshold, staleThreshold]);
669
- }
670
- });
671
- const allFactsRows = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL`, [entityId]);
672
- const allTasks = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}tasks WHERE entity_id = ? AND status IN ('pending', 'in_progress') AND deleted_at IS NULL`, [entityId]);
673
- const recentEvents = await this.db.getAllAsync(`SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at DESC LIMIT 20`, [entityId]);
674
- const healCandidates = allFactsRows.filter((f) => f.source_type !== "user_document");
675
- const documentAnchors = allFactsRows.filter((f) => f.source_type === "user_document").map(({ id, title, source_ref }) => ({ id, title, source_ref }));
676
- const userPrompt = `Heal Candidates:
677
- ${JSON.stringify(healCandidates.map((f) => ({ ...f, tags: typeof f.tags === "string" ? JSON.parse(f.tags) : f.tags })), null, 2)}
678
-
679
- Document Anchors (DO NOT MODIFY OR DELETE):
680
- ${JSON.stringify(documentAnchors, null, 2)}
681
-
682
- All Tasks:
683
- ${JSON.stringify(allTasks, null, 2)}
684
-
685
- Recent Events:
686
- ${JSON.stringify(recentEvents, null, 2)}
687
-
688
- The following document anchors are provided for contradiction detection only. Do not include them in \`downgraded\`, \`deleted\`, or \`newFacts\`.`;
689
- const responseText = await this.options.llmProvider.generateText({
690
- systemPrompt: HEAL_SYSTEM_PROMPT,
691
- userPrompt
692
- });
693
- const result = parseJsonResponse(responseText);
694
- const mutableIds = new Set(healCandidates.map((f) => f.id));
695
- const downgraded = Array.isArray(result.downgraded) ? result.downgraded : [];
696
- const deleted = Array.isArray(result.deleted) ? result.deleted : [];
697
- const newFacts = Array.isArray(result.newFacts) ? result.newFacts : [];
698
- const safeDowngraded = downgraded.filter((id) => mutableIds.has(id));
699
- const safeDeleted = deleted.filter((id) => mutableIds.has(id));
700
- const validNewFacts = newFacts.map(validateFact).filter((f) => f !== null);
701
- await this.db.withTransactionAsync(async () => {
702
- for (const id of safeDowngraded) {
703
- await this.db.runAsync(`UPDATE ${this.prefix}entries SET confidence = 'tentative', updated_at = ? WHERE id = ? AND entity_id = ?`, [now, id, entityId]);
704
- }
705
- for (const id of safeDeleted) {
706
- await this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE id = ? AND entity_id = ?`, [now, now, id, entityId]);
707
- }
708
- for (const fact of validNewFacts) {
709
- const id = generateId("fact_");
710
- await this.db.runAsync(`
711
- INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, created_at, updated_at)
712
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
713
- `, [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "agent_inferred", now, now]);
714
- }
715
- });
716
- }
717
- async runLibrarian(entityId) {
718
- const jobKey = this._librarianKey(entityId);
719
- if (this.activeMaintenanceJobs.has(jobKey)) {
720
- throw new WikiBusyError("librarian", entityId);
721
- }
722
- this.activeMaintenanceJobs.add(jobKey);
723
- try {
724
- await this._doRunLibrarian(entityId);
725
- } finally {
726
- this.activeMaintenanceJobs.delete(jobKey);
727
- }
728
- }
729
- async runHeal(entityId) {
730
- const jobKey = this._healKey(entityId);
731
- if (this.activeMaintenanceJobs.has(jobKey)) {
732
- throw new WikiBusyError("heal", entityId);
733
- }
734
- this.activeMaintenanceJobs.add(jobKey);
735
- try {
736
- await this._doRunHeal(entityId);
737
- } finally {
738
- this.activeMaintenanceJobs.delete(jobKey);
739
- }
740
- }
741
- getEntityStatus(entityId) {
742
- const ingestPrefix = `${this.prefix}:${entityId}:`;
743
- let ingesting = false;
744
- for (const k of this.activeIngestJobs) {
745
- if (k.startsWith(ingestPrefix)) {
746
- ingesting = true;
747
- break;
748
- }
749
- }
750
- return {
751
- ingesting,
752
- librarian: this.activeMaintenanceJobs.has(this._librarianKey(entityId)),
753
- heal: this.activeMaintenanceJobs.has(this._healKey(entityId))
754
- };
755
- }
756
- async _getFullBundle(entityId, opts) {
757
- const maxEvents = opts?.maxEvents;
758
- const eventsQuery = maxEvents != null ? `SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at DESC LIMIT ?` : `SELECT * FROM ${this.prefix}events WHERE entity_id = ? ORDER BY created_at ASC`;
759
- const eventsParams = maxEvents != null ? [entityId, maxEvents] : [entityId];
760
- const [factsRaw, tasks, eventsRaw] = await Promise.all([
761
- this.db.getAllAsync(
762
- `SELECT * FROM ${this.prefix}entries WHERE entity_id = ? AND deleted_at IS NULL ORDER BY updated_at DESC`,
763
- [entityId]
764
- ),
765
- this.db.getAllAsync(
766
- `SELECT * FROM ${this.prefix}tasks WHERE entity_id = ? AND deleted_at IS NULL ORDER BY priority DESC, created_at ASC`,
767
- [entityId]
768
- ),
769
- this.db.getAllAsync(eventsQuery, eventsParams)
770
- ]);
771
- const facts = factsRaw.map((f) => ({
772
- ...f,
773
- tags: typeof f.tags === "string" ? JSON.parse(f.tags) : f.tags
774
- }));
775
- const events = maxEvents != null ? eventsRaw.slice().reverse() : eventsRaw;
776
- return { facts, tasks, events };
777
- }
778
- async exportDump(entityIds) {
779
- let ids;
780
- if (entityIds && entityIds.length > 0) {
781
- ids = Array.from(new Set(entityIds));
782
- } else {
783
- const rows = await this.db.getAllAsync(`
784
- SELECT DISTINCT entity_id FROM (
785
- SELECT entity_id FROM ${this.prefix}entries WHERE deleted_at IS NULL
786
- UNION
787
- SELECT entity_id FROM ${this.prefix}tasks WHERE deleted_at IS NULL
788
- UNION
789
- SELECT entity_id FROM ${this.prefix}events
790
- ) ORDER BY entity_id
791
- `);
792
- ids = rows.map((r) => r.entity_id);
793
- }
794
- const entities = {};
795
- const BATCH = 3;
796
- for (let i = 0; i < ids.length; i += BATCH) {
797
- const batch = ids.slice(i, i + BATCH);
798
- const batchResults = await Promise.all(
799
- batch.map(async (id) => [id, await this._getFullBundle(id)])
800
- );
801
- for (const [id, bundle] of batchResults) {
802
- entities[id] = bundle;
803
- }
804
- }
805
- return { generatedAt: Date.now(), entities };
806
- }
807
- async importDump(dump, opts) {
808
- const merge = opts?.merge ?? false;
809
- for (const [entityId, bundle] of Object.entries(dump.entities)) {
810
- await this.db.withTransactionAsync(async () => {
811
- if (!merge) {
812
- const now = Date.now();
813
- await this.db.runAsync(
814
- `UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`,
815
- [now, now, entityId]
816
- );
817
- await this.db.runAsync(
818
- `UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`,
819
- [now, now, entityId]
820
- );
821
- await this.db.runAsync(
822
- `DELETE FROM ${this.prefix}checkpoints WHERE entity_id = ?`,
823
- [entityId]
824
- );
825
- }
826
- const factIds = bundle.facts.map((fact) => fact.id);
827
- const existingFactsById = /* @__PURE__ */ new Map();
828
- const factLookupChunkSize = 500;
829
- for (let i = 0; i < factIds.length; i += factLookupChunkSize) {
830
- const factIdChunk = factIds.slice(i, i + factLookupChunkSize);
831
- if (factIdChunk.length === 0) continue;
832
- const placeholders = factIdChunk.map(() => "?").join(", ");
833
- const existingFacts = await this.db.getAllAsync(
834
- `SELECT id, entity_id, updated_at FROM ${this.prefix}entries WHERE id IN (${placeholders})`,
835
- factIdChunk
836
- );
837
- for (const existingFact of existingFacts) {
838
- existingFactsById.set(existingFact.id, existingFact);
839
- }
840
- }
841
- for (const fact of bundle.facts) {
842
- const tagsJson = JSON.stringify(Array.isArray(fact.tags) ? fact.tags : []);
843
- const safeUpdatedAt = Number.isFinite(fact.updated_at) ? fact.updated_at : 0;
844
- const existing = existingFactsById.get(fact.id);
845
- if (existing) {
846
- if (existing.entity_id !== entityId) {
847
- this._warnCrossEntityCollision("entry", fact.id, existing.entity_id, entityId);
848
- continue;
849
- }
850
- if (merge) {
851
- if (safeUpdatedAt <= existing.updated_at) continue;
852
- }
853
- await this.db.runAsync(
854
- `UPDATE ${this.prefix}entries SET entity_id = ?, title = ?, body = ?, tags = ?, confidence = ?, source_type = ?, source_hash = ?, source_ref = ?, created_at = ?, updated_at = ?, last_accessed_at = ?, access_count = ?, deleted_at = ? WHERE id = ?`,
855
- [entityId, fact.title, fact.body, tagsJson, fact.confidence, fact.source_type, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at, fact.id]
856
- );
857
- existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
858
- } else {
859
- await this.db.runAsync(
860
- `INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
861
- [fact.id, entityId, fact.title, fact.body, tagsJson, fact.confidence, fact.source_type, fact.source_hash, fact.source_ref, fact.created_at, safeUpdatedAt, fact.last_accessed_at, fact.access_count, fact.deleted_at]
862
- );
863
- existingFactsById.set(fact.id, { id: fact.id, entity_id: entityId, updated_at: safeUpdatedAt });
864
- }
865
- }
866
- const taskIds = bundle.tasks.map((task) => task.id);
867
- const existingTasksById = /* @__PURE__ */ new Map();
868
- const taskLookupChunkSize = 500;
869
- for (let i = 0; i < taskIds.length; i += taskLookupChunkSize) {
870
- const taskIdChunk = taskIds.slice(i, i + taskLookupChunkSize);
871
- if (taskIdChunk.length === 0) continue;
872
- const placeholders = taskIdChunk.map(() => "?").join(", ");
873
- const existingTasks = await this.db.getAllAsync(
874
- `SELECT id, entity_id, updated_at FROM ${this.prefix}tasks WHERE id IN (${placeholders})`,
875
- taskIdChunk
876
- );
877
- for (const existingTask of existingTasks) {
878
- existingTasksById.set(existingTask.id, existingTask);
879
- }
880
- }
881
- for (const task of bundle.tasks) {
882
- const safeUpdatedAt = Number.isFinite(task.updated_at) ? task.updated_at : 0;
883
- const existing = existingTasksById.get(task.id);
884
- if (existing) {
885
- if (existing.entity_id !== entityId) {
886
- this._warnCrossEntityCollision("task", task.id, existing.entity_id, entityId);
887
- continue;
888
- }
889
- if (merge) {
890
- if (safeUpdatedAt <= existing.updated_at) continue;
891
- }
892
- await this.db.runAsync(
893
- `UPDATE ${this.prefix}tasks SET entity_id = ?, description = ?, status = ?, priority = ?, created_at = ?, updated_at = ?, resolved_at = ?, deleted_at = ? WHERE id = ?`,
894
- [entityId, task.description, task.status, task.priority, task.created_at, safeUpdatedAt, task.resolved_at, task.deleted_at, task.id]
895
- );
896
- existingTasksById.set(task.id, { id: task.id, entity_id: entityId, updated_at: safeUpdatedAt });
897
- } else {
898
- await this.db.runAsync(
899
- `INSERT INTO ${this.prefix}tasks (id, entity_id, description, status, priority, created_at, updated_at, resolved_at, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
900
- [task.id, entityId, task.description, task.status, task.priority, task.created_at, safeUpdatedAt, task.resolved_at, task.deleted_at]
901
- );
902
- existingTasksById.set(task.id, { id: task.id, entity_id: entityId, updated_at: safeUpdatedAt });
903
- }
904
- }
905
- for (const event of bundle.events) {
906
- await this.db.runAsync(
907
- `INSERT OR IGNORE INTO ${this.prefix}events (id, entity_id, event_type, summary, related_entry_id, created_at)
908
- VALUES (?, ?, ?, ?, ?, ?)`,
909
- [event.id, entityId, event.event_type, event.summary, event.related_entry_id ?? null, event.created_at]
910
- );
911
- }
912
- });
913
- }
914
- }
915
- async forget(entityId, params) {
916
- const now = Date.now();
917
- let deletedEntries = 0;
918
- let deletedTasks = 0;
919
- if (params.clearAll) {
920
- const [entriesRes, tasksRes] = await Promise.all([
921
- this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId]),
922
- this.db.runAsync(`UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`, [now, now, entityId])
923
- ]);
924
- await this.db.runAsync(`UPDATE ${this.prefix}checkpoints SET memory_checkpoint = 0, heal_checkpoint = 0 WHERE entity_id = ?`, [entityId]);
925
- deletedEntries = entriesRes.changes;
926
- deletedTasks = tasksRes.changes;
927
- } else {
928
- const hasIdSelectors = params.entryId !== void 0 || params.taskId !== void 0;
929
- const hasSourceSelectors = params.sourceRef !== void 0 || params.sourceHash !== void 0;
930
- if (hasIdSelectors && hasSourceSelectors) {
931
- throw new Error("forget() params are mutually exclusive: use entryId/taskId together, or sourceRef/sourceHash together, but not both in the same call");
932
- }
933
- const sourceRef = params.sourceRef !== void 0 ? normalizeSourceRef(params.sourceRef) : null;
934
- if (params.sourceRef !== void 0 && !sourceRef) throw new Error("Invalid sourceRef");
935
- const sourceHash = params.sourceHash !== void 0 ? normalizeSourceHash(params.sourceHash) : null;
936
- if (params.sourceHash !== void 0 && !sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
937
- const entryPromise = params.entryId ? this.db.runAsync(`UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`, [now, now, params.entryId, entityId]) : null;
938
- const taskPromise = params.taskId ? this.db.runAsync(`UPDATE ${this.prefix}tasks SET deleted_at = ?, updated_at = ? WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`, [now, now, params.taskId, entityId]) : null;
939
- let refPromise = null;
940
- if (sourceRef || sourceHash) {
941
- let q = `UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE entity_id = ? AND deleted_at IS NULL`;
942
- const args = [now, now, entityId];
943
- if (sourceRef) {
944
- q += ` AND source_ref = ?`;
945
- args.push(sourceRef);
946
- }
947
- if (sourceHash) {
948
- q += ` AND source_hash = ?`;
949
- args.push(sourceHash);
950
- }
951
- refPromise = this.db.runAsync(q, args);
952
- }
953
- const [entryResult, taskResult, refResult] = await Promise.all([
954
- entryPromise ?? Promise.resolve(null),
955
- taskPromise ?? Promise.resolve(null),
956
- refPromise ?? Promise.resolve(null)
957
- ]);
958
- if (entryResult) deletedEntries += entryResult.changes;
959
- if (taskResult) deletedTasks += taskResult.changes;
960
- if (refResult) deletedEntries += refResult.changes;
961
- }
962
- return { deleted: { entries: deletedEntries, tasks: deletedTasks } };
963
- }
964
- async ingestDocument(entityId, params) {
965
- const sourceRef = normalizeSourceRef(params.sourceRef);
966
- if (!sourceRef) throw new Error("Invalid sourceRef");
967
- const sourceHash = normalizeSourceHash(params.sourceHash);
968
- if (!sourceHash) throw new Error("Invalid sourceHash (must be 64-char hex string)");
969
- const maxChunkLength = params.maxChunkLength ?? this.options.config?.maxChunkLength ?? 12e3;
970
- const rawOverlap = params.chunkOverlap ?? this.options.config?.chunkOverlap ?? 400;
971
- const chunkOverlap = Math.min(
972
- Number.isFinite(rawOverlap) && rawOverlap >= 0 ? Math.floor(rawOverlap) : 400,
973
- maxChunkLength - 1
974
- );
975
- const rawConcurrency = params.chunkConcurrency ?? this.options.config?.chunkConcurrency ?? 1;
976
- const chunkConcurrency = Number.isFinite(rawConcurrency) && rawConcurrency >= 1 ? Math.floor(rawConcurrency) : 1;
977
- if (typeof params.documentChunk !== "string") {
978
- throw new Error(`documentChunk must be a string, received ${typeof params.documentChunk}`);
979
- }
980
- const jobKey = `${this.prefix}:${entityId}:${sourceRef}`;
981
- if (this.activeIngestJobs.has(jobKey)) {
982
- throw new WikiBusyError("ingest", entityId);
983
- }
984
- this.activeIngestJobs.add(jobKey);
985
- try {
986
- const { chunks, truncated } = chunkText(params.documentChunk, maxChunkLength, chunkOverlap);
987
- if (chunks.length === 0) {
988
- return { truncated: false, chunks: 0 };
989
- }
990
- const chunkResults = await withConcurrency(
991
- chunks.map((chunk) => async () => {
992
- const userPrompt = `Document Chunk:
993
- ${chunk}`;
994
- const responseText = await this.options.llmProvider.generateText({
995
- systemPrompt: INGEST_SYSTEM_PROMPT,
996
- userPrompt
997
- });
998
- const result = parseJsonResponse(responseText);
999
- return (Array.isArray(result.facts) ? result.facts : []).map(validateFact).filter((f) => f !== null);
1000
- }),
1001
- chunkConcurrency
1002
- );
1003
- const seen = /* @__PURE__ */ new Set();
1004
- const allValidFacts = [];
1005
- for (const facts of chunkResults) {
1006
- for (const fact of facts) {
1007
- const normalized = fact.title.trim().toLowerCase().replace(/\s+/g, " ");
1008
- if (!seen.has(normalized)) {
1009
- seen.add(normalized);
1010
- allValidFacts.push(fact);
1011
- }
1012
- }
1013
- }
1014
- const now = Date.now();
1015
- await this.db.withTransactionAsync(async () => {
1016
- await this.db.runAsync(
1017
- `UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE source_ref = ? AND entity_id = ? AND deleted_at IS NULL`,
1018
- [now, now, sourceRef, entityId]
1019
- );
1020
- for (const fact of allValidFacts) {
1021
- const id = generateId("fact_");
1022
- await this.db.runAsync(
1023
- `INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at)
1024
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1025
- [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "user_document", sourceHash, sourceRef, now, now]
1026
- );
1027
- }
1028
- });
1029
- return { truncated, chunks: chunks.length };
1030
- } finally {
1031
- this.activeIngestJobs.delete(jobKey);
1032
- }
1033
- }
1034
- };
1035
-
1036
- // src/utils/formatMemoryDump.ts
1037
- function renderFact(f) {
1038
- const tags = (f.tags || []).join(", ");
1039
- const source = f.source_ref ?? f.source_type;
1040
- return `### ${f.title}
1041
- **Tags:** ${tags}
1042
- **Confidence:** ${f.confidence}
1043
- **Source:** ${source}
1044
-
1045
- ${f.body}
1046
-
1047
- ---
1048
- `;
1049
- }
1050
- function renderTask(t) {
1051
- const checked = t.status === "done" ? "x" : " ";
1052
- const note = t.status === "done" ? " (done)" : t.status === "abandoned" ? " (abandoned)" : t.status === "in_progress" ? " (in progress)" : "";
1053
- return `- [${checked}] ${t.description}${note}
1054
- `;
1055
- }
1056
- function renderEvent(e) {
1057
- const ts = new Date(e.created_at).toISOString();
1058
- return `- [${ts}] (${e.event_type}) ${e.summary}
1059
- `;
1060
- }
1061
- function renderEntity(entityId, bundle, generatedAt) {
1062
- const lines = [];
1063
- lines.push(`# Memory Dump: ${entityId}`);
1064
- lines.push(`Generated: ${new Date(generatedAt).toISOString()}`);
1065
- lines.push("");
1066
- lines.push("## Facts");
1067
- lines.push("");
1068
- if (bundle.facts.length === 0) {
1069
- lines.push("_(none)_\n");
1070
- } else {
1071
- for (const f of bundle.facts) lines.push(renderFact(f));
1072
- }
1073
- lines.push("## Tasks");
1074
- lines.push("");
1075
- if (bundle.tasks.length === 0) {
1076
- lines.push("_(none)_\n");
1077
- } else {
1078
- for (const t of bundle.tasks) lines.push(renderTask(t));
1079
- }
1080
- lines.push("");
1081
- lines.push("## Recent Events");
1082
- lines.push("");
1083
- if (bundle.events.length === 0) {
1084
- lines.push("_(none)_\n");
1085
- } else {
1086
- for (const e of bundle.events) lines.push(renderEvent(e));
1087
- }
1088
- return lines.join("\n");
1089
- }
1090
- function shortHash(value) {
1091
- let h1 = 5381;
1092
- let h2 = 52711;
1093
- for (let i = 0; i < value.length; i += 1) {
1094
- const c = value.charCodeAt(i);
1095
- h1 = Math.imul(h1, 33) ^ c;
1096
- h2 = Math.imul(h2, 31) ^ c;
1097
- }
1098
- return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
1099
- }
1100
- function formatEntityFileName(entityId) {
1101
- const normalized = entityId.normalize("NFKC");
1102
- const sanitized = normalized.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "_").replace(/_+/g, "_").replace(/^[_-]+|[_-]+$/g, "");
1103
- const MAX_BASE = 200;
1104
- const trimmed = sanitized.length > MAX_BASE ? sanitized.slice(0, MAX_BASE) : sanitized;
1105
- const baseName = trimmed && trimmed !== "." && trimmed !== ".." ? trimmed : "entity";
1106
- const needsSuffix = baseName !== entityId || sanitized.length > MAX_BASE;
1107
- const uniqueBaseName = needsSuffix ? `${baseName}-${shortHash(entityId)}` : baseName;
1108
- return `${uniqueBaseName}.md`;
1109
- }
1110
- function formatMemoryDump(dump) {
1111
- const files = Object.entries(dump.entities).map(([entityId, bundle]) => ({
1112
- name: formatEntityFileName(entityId),
1113
- content: renderEntity(entityId, bundle, dump.generatedAt)
1114
- }));
29
+ // src/adapter.ts
30
+ function createExpoAdapter(db) {
1115
31
  return {
1116
- manifest: JSON.stringify(dump, null, 2),
1117
- files
32
+ execAsync: (sql) => db.execAsync(sql),
33
+ runAsync: async (sql, params = []) => {
34
+ const result = await db.runAsync(sql, params);
35
+ return { changes: result.changes, lastInsertRowId: result.lastInsertRowId };
36
+ },
37
+ getAllAsync: (sql, params = []) => db.getAllAsync(sql, params),
38
+ getFirstAsync: (sql, params = []) => db.getFirstAsync(sql, params),
39
+ withTransactionAsync: (fn) => {
40
+ let captured;
41
+ return db.withTransactionAsync(() => fn().then((v) => {
42
+ captured = v;
43
+ })).then(() => captured);
44
+ },
45
+ closeAsync: () => db.closeAsync()
1118
46
  };
1119
47
  }
1120
48
 
1121
49
  // src/index.ts
50
+ __reExport(index_exports, require("@equationalapplications/core-llm-wiki"), module.exports);
51
+ __reExport(index_exports, require("@equationalapplications/react-llm-wiki"), module.exports);
1122
52
  function createWiki(db, options) {
1123
- return new WikiMemory(db, options);
53
+ return new import_core_llm_wiki.WikiMemory(createExpoAdapter(db), options);
1124
54
  }
1125
55
  // Annotate the CommonJS export names for ESM import in node:
1126
56
  0 && (module.exports = {
1127
- WikiBusyError,
1128
- WikiMemory,
1129
57
  createWiki,
1130
- formatMemoryDump
58
+ ...require("@equationalapplications/core-llm-wiki"),
59
+ ...require("@equationalapplications/react-llm-wiki")
1131
60
  });
61
+ //# sourceMappingURL=index.js.map