@equationalapplications/expo-llm-wiki 2.2.0 → 2.3.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
@@ -23,6 +23,7 @@ __export(index_exports, {
23
23
  WikiBusyError: () => WikiBusyError,
24
24
  WikiMemory: () => WikiMemory,
25
25
  createWiki: () => createWiki,
26
+ formatContext: () => formatContext,
26
27
  formatMemoryDump: () => formatMemoryDump
27
28
  });
28
29
  module.exports = __toCommonJS(index_exports);
@@ -110,9 +111,64 @@ async function setupDatabase(db, prefix) {
110
111
  heal_checkpoint INTEGER NOT NULL DEFAULT 0,
111
112
  memory_checkpoint INTEGER NOT NULL DEFAULT 0
112
113
  );
114
+
115
+ CREATE TABLE IF NOT EXISTS ${prefix}meta (
116
+ key TEXT PRIMARY KEY,
117
+ value TEXT NOT NULL
118
+ );
113
119
  `);
114
120
  }
115
121
 
122
+ // src/db/migrations.ts
123
+ var MIGRATIONS = [
124
+ {
125
+ version: 1,
126
+ description: "Rebuild FTS5 with porter unicode61 tokenizer",
127
+ run: async (db, prefix) => {
128
+ await db.withTransactionAsync(async () => {
129
+ await db.execAsync(`
130
+ DROP TRIGGER IF EXISTS ${prefix}entries_ai;
131
+ DROP TRIGGER IF EXISTS ${prefix}entries_ad;
132
+ DROP TRIGGER IF EXISTS ${prefix}entries_au;
133
+ DROP TABLE IF EXISTS ${prefix}entries_fts;
134
+ CREATE VIRTUAL TABLE ${prefix}entries_fts USING fts5(
135
+ title,
136
+ body,
137
+ tags,
138
+ content='${prefix}entries',
139
+ content_rowid='rowid',
140
+ tokenize='porter unicode61'
141
+ );
142
+ INSERT INTO ${prefix}entries_fts(rowid, title, body, tags)
143
+ SELECT rowid, title, body, tags FROM ${prefix}entries;
144
+ CREATE TRIGGER ${prefix}entries_ai AFTER INSERT ON ${prefix}entries BEGIN
145
+ INSERT INTO ${prefix}entries_fts(rowid, title, body, tags)
146
+ VALUES (new.rowid, new.title, new.body, new.tags);
147
+ END;
148
+ CREATE TRIGGER ${prefix}entries_ad AFTER DELETE ON ${prefix}entries BEGIN
149
+ INSERT INTO ${prefix}entries_fts(${prefix}entries_fts, rowid, title, body, tags)
150
+ VALUES ('delete', old.rowid, old.title, old.body, old.tags);
151
+ END;
152
+ CREATE TRIGGER ${prefix}entries_au AFTER UPDATE ON ${prefix}entries BEGIN
153
+ INSERT INTO ${prefix}entries_fts(${prefix}entries_fts, rowid, title, body, tags)
154
+ VALUES ('delete', old.rowid, old.title, old.body, old.tags);
155
+ INSERT INTO ${prefix}entries_fts(rowid, title, body, tags)
156
+ VALUES (new.rowid, new.title, new.body, new.tags);
157
+ END;
158
+ `);
159
+ });
160
+ }
161
+ }
162
+ ];
163
+ for (let i = 1; i < MIGRATIONS.length; i++) {
164
+ if (MIGRATIONS[i].version <= MIGRATIONS[i - 1].version) {
165
+ throw new Error(
166
+ `migrations.ts: MIGRATIONS must be in strictly ascending version order. Found version ${MIGRATIONS[i].version} after ${MIGRATIONS[i - 1].version} at index ${i}.`
167
+ );
168
+ }
169
+ }
170
+ var CURRENT_SCHEMA_VERSION = MIGRATIONS.length > 0 ? MIGRATIONS[MIGRATIONS.length - 1].version : 0;
171
+
116
172
  // src/types.ts
117
173
  var WikiBusyError = class extends Error {
118
174
  operation;
@@ -373,45 +429,54 @@ var WikiMemory = class {
373
429
  this.prefix = options.config?.tablePrefix || "llm_wiki_";
374
430
  }
375
431
  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`]
432
+ const entriesExistedBeforeSetup = await this.db.getFirstAsync(
433
+ `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
434
+ [`${this.prefix}entries`]
380
435
  );
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
- });
436
+ await setupDatabase(this.db, this.prefix);
437
+ let currentVersion;
438
+ if (!entriesExistedBeforeSetup) {
439
+ await this.db.runAsync(
440
+ `INSERT OR REPLACE INTO ${this.prefix}meta (key, value) VALUES ('schema_version', ?)`,
441
+ [String(CURRENT_SCHEMA_VERSION)]
442
+ );
443
+ currentVersion = CURRENT_SCHEMA_VERSION;
444
+ } else {
445
+ const metaRow = await this.db.getFirstAsync(
446
+ `SELECT value FROM ${this.prefix}meta WHERE key = 'schema_version'`
447
+ );
448
+ if (metaRow) {
449
+ currentVersion = parseInt(metaRow.value, 10);
450
+ if (!Number.isFinite(currentVersion)) currentVersion = 0;
451
+ } else {
452
+ const ftsMeta = await this.db.getFirstAsync(
453
+ `SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
454
+ [`${this.prefix}entries_fts`]
455
+ );
456
+ const hasPorter = /tokenize\s*=\s*['"]porter\s+unicode61['"]/i.test(ftsMeta?.sql ?? "");
457
+ currentVersion = hasPorter ? 1 : 0;
458
+ }
459
+ }
460
+ for (const migration of MIGRATIONS) {
461
+ if (migration.version > currentVersion) {
462
+ await migration.run(this.db, this.prefix);
463
+ await this.db.runAsync(
464
+ `INSERT OR REPLACE INTO ${this.prefix}meta (key, value) VALUES ('schema_version', ?)`,
465
+ [String(migration.version)]
466
+ );
467
+ currentVersion = migration.version;
468
+ }
469
+ }
470
+ if (entriesExistedBeforeSetup) {
471
+ const metaCheck = await this.db.getFirstAsync(
472
+ `SELECT value FROM ${this.prefix}meta WHERE key = 'schema_version'`
473
+ );
474
+ if (!metaCheck) {
475
+ await this.db.runAsync(
476
+ `INSERT OR REPLACE INTO ${this.prefix}meta (key, value) VALUES ('schema_version', ?)`,
477
+ [String(currentVersion)]
478
+ );
479
+ }
415
480
  }
416
481
  const rows = await this.db.getAllAsync(`
417
482
  SELECT rowid, source_ref FROM ${this.prefix}entries
@@ -436,6 +501,101 @@ var WikiMemory = class {
436
501
  }
437
502
  });
438
503
  }
504
+ async hasChanged(entityId, sourceRef, sourceHash) {
505
+ const normalizedRef = normalizeSourceRef(sourceRef);
506
+ if (!normalizedRef) {
507
+ throw new Error(`Invalid sourceRef: "${sourceRef}"`);
508
+ }
509
+ const normalizedHash = normalizeSourceHash(sourceHash);
510
+ if (!normalizedHash) {
511
+ throw new Error(`Invalid sourceHash: must be a 64-character hex string (normalized to lowercase)`);
512
+ }
513
+ const row = await this.db.getFirstAsync(
514
+ `SELECT source_hash FROM ${this.prefix}entries
515
+ WHERE entity_id = ? AND source_ref = ? AND deleted_at IS NULL
516
+ ORDER BY updated_at DESC
517
+ LIMIT 1`,
518
+ [entityId, normalizedRef]
519
+ );
520
+ if (!row) return true;
521
+ const normalizedStoredHash = row.source_hash ? normalizeSourceHash(row.source_hash) : null;
522
+ return normalizedStoredHash !== normalizedHash;
523
+ }
524
+ _pruneKey(entityId) {
525
+ return `${this.prefix}:${entityId}:prune`;
526
+ }
527
+ _validatePruneDuration(value, name) {
528
+ if (value !== null && value !== void 0 && (typeof value !== "number" || !isFinite(value) || value < 0)) {
529
+ throw new Error(`Invalid ${name}: must be a non-negative finite number or null`);
530
+ }
531
+ }
532
+ async runPrune(entityId, options) {
533
+ const pruneKey = this._pruneKey(entityId);
534
+ const ingestPrefix = `${this.prefix}:${entityId}:`;
535
+ let isIngestRunning = false;
536
+ for (const k of this.activeIngestJobs) {
537
+ if (k.startsWith(ingestPrefix)) {
538
+ isIngestRunning = true;
539
+ break;
540
+ }
541
+ }
542
+ let blockingOperation = null;
543
+ if (this.activeMaintenanceJobs.has(pruneKey)) {
544
+ blockingOperation = "prune";
545
+ } else if (this.activeMaintenanceJobs.has(this._librarianKey(entityId))) {
546
+ blockingOperation = "librarian";
547
+ } else if (this.activeMaintenanceJobs.has(this._healKey(entityId))) {
548
+ blockingOperation = "heal";
549
+ } else if (isIngestRunning) {
550
+ blockingOperation = "ingest";
551
+ }
552
+ if (blockingOperation !== null) {
553
+ throw new WikiBusyError(blockingOperation, entityId);
554
+ }
555
+ this.activeMaintenanceJobs.add(pruneKey);
556
+ try {
557
+ const retainSoftDeletedFor = options?.retainSoftDeletedFor !== void 0 ? options.retainSoftDeletedFor : this.options.config?.pruneRetainSoftDeletedFor ?? 7;
558
+ const retainEventsFor = options?.retainEventsFor !== void 0 ? options.retainEventsFor : this.options.config?.pruneEventsAfter ?? 30;
559
+ const vacuum = options?.vacuum ?? false;
560
+ this._validatePruneDuration(retainSoftDeletedFor, "retainSoftDeletedFor");
561
+ this._validatePruneDuration(retainEventsFor, "retainEventsFor");
562
+ const now = Date.now();
563
+ let deletedEntries = 0;
564
+ let deletedTasks = 0;
565
+ let deletedEvents = 0;
566
+ if (retainSoftDeletedFor !== null) {
567
+ const cutoff = now - retainSoftDeletedFor * 864e5;
568
+ const entryResult = await this.db.runAsync(
569
+ `DELETE FROM ${this.prefix}entries
570
+ WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
571
+ [entityId, cutoff]
572
+ );
573
+ deletedEntries = entryResult.changes;
574
+ const taskResult = await this.db.runAsync(
575
+ `DELETE FROM ${this.prefix}tasks
576
+ WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
577
+ [entityId, cutoff]
578
+ );
579
+ deletedTasks = taskResult.changes;
580
+ }
581
+ if (retainEventsFor !== null) {
582
+ const cutoff = now - retainEventsFor * 864e5;
583
+ const eventResult = await this.db.runAsync(
584
+ `DELETE FROM ${this.prefix}events
585
+ WHERE entity_id = ? AND created_at < ?`,
586
+ [entityId, cutoff]
587
+ );
588
+ deletedEvents = eventResult.changes;
589
+ }
590
+ if (vacuum) {
591
+ await this.db.execAsync(`PRAGMA wal_checkpoint(TRUNCATE)`);
592
+ await this.db.execAsync(`VACUUM`);
593
+ }
594
+ return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
595
+ } finally {
596
+ this.activeMaintenanceJobs.delete(pruneKey);
597
+ }
598
+ }
439
599
  formatSearchQuery(query) {
440
600
  const normalizeTokens = (value) => value.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((t) => t.length >= 3);
441
601
  const baseTokens = normalizeTokens(query);
@@ -541,7 +701,7 @@ var WikiMemory = class {
541
701
  if (memoryCheckpoint > count) memoryCheckpoint = 0;
542
702
  if (count - memoryCheckpoint >= threshold) {
543
703
  const jobKey = this._librarianKey(entityId);
544
- if (!this.activeMaintenanceJobs.has(jobKey)) {
704
+ if (!this.activeMaintenanceJobs.has(jobKey) && !this.activeMaintenanceJobs.has(this._pruneKey(entityId))) {
545
705
  this.activeMaintenanceJobs.add(jobKey);
546
706
  this.runLibrarianThenMaybeHeal(entityId, count).catch(console.error).finally(() => this.activeMaintenanceJobs.delete(jobKey));
547
707
  }
@@ -719,6 +879,9 @@ The following document anchors are provided for contradiction detection only. Do
719
879
  if (this.activeMaintenanceJobs.has(jobKey)) {
720
880
  throw new WikiBusyError("librarian", entityId);
721
881
  }
882
+ if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) {
883
+ throw new WikiBusyError("prune", entityId);
884
+ }
722
885
  this.activeMaintenanceJobs.add(jobKey);
723
886
  try {
724
887
  await this._doRunLibrarian(entityId);
@@ -731,6 +894,9 @@ The following document anchors are provided for contradiction detection only. Do
731
894
  if (this.activeMaintenanceJobs.has(jobKey)) {
732
895
  throw new WikiBusyError("heal", entityId);
733
896
  }
897
+ if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) {
898
+ throw new WikiBusyError("prune", entityId);
899
+ }
734
900
  this.activeMaintenanceJobs.add(jobKey);
735
901
  try {
736
902
  await this._doRunHeal(entityId);
@@ -981,6 +1147,9 @@ The following document anchors are provided for contradiction detection only. Do
981
1147
  if (this.activeIngestJobs.has(jobKey)) {
982
1148
  throw new WikiBusyError("ingest", entityId);
983
1149
  }
1150
+ if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) {
1151
+ throw new WikiBusyError("prune", entityId);
1152
+ }
984
1153
  this.activeIngestJobs.add(jobKey);
985
1154
  try {
986
1155
  const { chunks, truncated } = chunkText(params.documentChunk, maxChunkLength, chunkOverlap);
@@ -1118,6 +1287,121 @@ function formatMemoryDump(dump) {
1118
1287
  };
1119
1288
  }
1120
1289
 
1290
+ // src/utils/formatContext.ts
1291
+ function validateMaxOption(value, name) {
1292
+ if (!isFinite(value) || value < 0) {
1293
+ throw new Error(`Invalid ${name}: must be a non-negative finite number`);
1294
+ }
1295
+ }
1296
+ var CONFIDENCE_WEIGHT = {
1297
+ certain: 1,
1298
+ inferred: 0.6,
1299
+ tentative: 0.3
1300
+ };
1301
+ function scoreFactFor(fact, weights, now) {
1302
+ const confW = CONFIDENCE_WEIGHT[fact.confidence] ?? 0.3;
1303
+ const ageDays = (now - fact.updated_at) / 864e5;
1304
+ const recencyDecay = Math.exp(-ageDays / 30);
1305
+ return confW * weights.confidence + Math.log(1 + fact.access_count) * weights.accessCount + recencyDecay * weights.recency;
1306
+ }
1307
+ function renderFactMarkdown(fact, includeConfidence, includeTags) {
1308
+ const confPart = includeConfidence ? ` (${fact.confidence})` : "";
1309
+ const tagPart = includeTags && fact.tags.length > 0 ? ` [${fact.tags.join(", ")}]` : "";
1310
+ return `- **${fact.title}**${confPart}${tagPart}
1311
+ ${fact.body.replace(/\n/g, "\n ")}`;
1312
+ }
1313
+ function renderFactPlain(fact, includeConfidence, includeTags) {
1314
+ const confPart = includeConfidence ? ` (${fact.confidence})` : "";
1315
+ const tagPart = includeTags && fact.tags.length > 0 ? ` [${fact.tags.join(", ")}]` : "";
1316
+ return `${fact.title}${confPart}${tagPart}: ${fact.body}`;
1317
+ }
1318
+ function renderTaskMarkdown(task) {
1319
+ return `- [P${task.priority}] ${task.description.replace(/\n/g, "\n ")} (${task.status})`;
1320
+ }
1321
+ function renderTaskPlain(task) {
1322
+ return `[P${task.priority}] ${task.description} (${task.status})`;
1323
+ }
1324
+ function renderEventMarkdown(event) {
1325
+ const ts = new Date(event.created_at).toISOString();
1326
+ return `- [${event.event_type} @ ${ts}] ${event.summary.replace(/\n/g, "\n ")}`;
1327
+ }
1328
+ function renderEventPlain(event) {
1329
+ const ts = new Date(event.created_at).toISOString();
1330
+ return `[${event.event_type} @ ${ts}] ${event.summary}`;
1331
+ }
1332
+ function formatContext(bundle, options) {
1333
+ const opts = {
1334
+ format: options?.format ?? "markdown",
1335
+ maxFacts: options?.maxFacts ?? 10,
1336
+ maxTasks: options?.maxTasks ?? 10,
1337
+ maxEvents: options?.maxEvents ?? 10,
1338
+ includeConfidence: options?.includeConfidence ?? true,
1339
+ includeTags: options?.includeTags ?? true,
1340
+ factWeights: {
1341
+ confidence: options?.factWeights?.confidence ?? 1,
1342
+ accessCount: options?.factWeights?.accessCount ?? 0.3,
1343
+ recency: options?.factWeights?.recency ?? 0.5
1344
+ }
1345
+ };
1346
+ validateMaxOption(opts.maxFacts, "maxFacts");
1347
+ validateMaxOption(opts.maxTasks, "maxTasks");
1348
+ validateMaxOption(opts.maxEvents, "maxEvents");
1349
+ const weights = opts.factWeights;
1350
+ const now = Date.now();
1351
+ const sortedFacts = [...bundle.facts].sort((a, b) => scoreFactFor(b, weights, now) - scoreFactFor(a, weights, now)).slice(0, opts.maxFacts);
1352
+ const sortedTasks = [...bundle.tasks].sort((a, b) => b.priority - a.priority || a.created_at - b.created_at).slice(0, opts.maxTasks);
1353
+ const sortedEvents = [...bundle.events].sort((a, b) => b.created_at - a.created_at).slice(0, opts.maxEvents);
1354
+ if (sortedFacts.length === 0 && sortedTasks.length === 0 && sortedEvents.length === 0) {
1355
+ return "";
1356
+ }
1357
+ const isMarkdown = opts.format === "markdown";
1358
+ const lines = [];
1359
+ if (isMarkdown) {
1360
+ lines.push("## Memory");
1361
+ if (sortedFacts.length > 0) {
1362
+ lines.push("");
1363
+ lines.push("### Known Facts");
1364
+ for (const fact of sortedFacts) {
1365
+ lines.push(renderFactMarkdown(fact, opts.includeConfidence, opts.includeTags));
1366
+ }
1367
+ }
1368
+ if (sortedTasks.length > 0) {
1369
+ lines.push("");
1370
+ lines.push("### Open Tasks");
1371
+ for (const task of sortedTasks) {
1372
+ lines.push(renderTaskMarkdown(task));
1373
+ }
1374
+ }
1375
+ if (sortedEvents.length > 0) {
1376
+ lines.push("");
1377
+ lines.push("### Recent Events");
1378
+ for (const event of sortedEvents) {
1379
+ lines.push(renderEventMarkdown(event));
1380
+ }
1381
+ }
1382
+ } else {
1383
+ if (sortedFacts.length > 0) {
1384
+ lines.push("KNOWN FACTS:");
1385
+ for (const fact of sortedFacts) {
1386
+ lines.push(renderFactPlain(fact, opts.includeConfidence, opts.includeTags));
1387
+ }
1388
+ }
1389
+ if (sortedTasks.length > 0) {
1390
+ lines.push("OPEN TASKS:");
1391
+ for (const task of sortedTasks) {
1392
+ lines.push(renderTaskPlain(task));
1393
+ }
1394
+ }
1395
+ if (sortedEvents.length > 0) {
1396
+ lines.push("RECENT EVENTS:");
1397
+ for (const event of sortedEvents) {
1398
+ lines.push(renderEventPlain(event));
1399
+ }
1400
+ }
1401
+ }
1402
+ return lines.join("\n");
1403
+ }
1404
+
1121
1405
  // src/index.ts
1122
1406
  function createWiki(db, options) {
1123
1407
  return new WikiMemory(db, options);
@@ -1127,5 +1411,6 @@ function createWiki(db, options) {
1127
1411
  WikiBusyError,
1128
1412
  WikiMemory,
1129
1413
  createWiki,
1414
+ formatContext,
1130
1415
  formatMemoryDump
1131
1416
  });