@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.mjs CHANGED
@@ -81,9 +81,64 @@ async function setupDatabase(db, prefix) {
81
81
  heal_checkpoint INTEGER NOT NULL DEFAULT 0,
82
82
  memory_checkpoint INTEGER NOT NULL DEFAULT 0
83
83
  );
84
+
85
+ CREATE TABLE IF NOT EXISTS ${prefix}meta (
86
+ key TEXT PRIMARY KEY,
87
+ value TEXT NOT NULL
88
+ );
84
89
  `);
85
90
  }
86
91
 
92
+ // src/db/migrations.ts
93
+ var MIGRATIONS = [
94
+ {
95
+ version: 1,
96
+ description: "Rebuild FTS5 with porter unicode61 tokenizer",
97
+ run: async (db, prefix) => {
98
+ await db.withTransactionAsync(async () => {
99
+ await db.execAsync(`
100
+ DROP TRIGGER IF EXISTS ${prefix}entries_ai;
101
+ DROP TRIGGER IF EXISTS ${prefix}entries_ad;
102
+ DROP TRIGGER IF EXISTS ${prefix}entries_au;
103
+ DROP TABLE IF EXISTS ${prefix}entries_fts;
104
+ CREATE VIRTUAL TABLE ${prefix}entries_fts USING fts5(
105
+ title,
106
+ body,
107
+ tags,
108
+ content='${prefix}entries',
109
+ content_rowid='rowid',
110
+ tokenize='porter unicode61'
111
+ );
112
+ INSERT INTO ${prefix}entries_fts(rowid, title, body, tags)
113
+ SELECT rowid, title, body, tags FROM ${prefix}entries;
114
+ CREATE TRIGGER ${prefix}entries_ai AFTER INSERT ON ${prefix}entries BEGIN
115
+ INSERT INTO ${prefix}entries_fts(rowid, title, body, tags)
116
+ VALUES (new.rowid, new.title, new.body, new.tags);
117
+ END;
118
+ CREATE TRIGGER ${prefix}entries_ad AFTER DELETE ON ${prefix}entries BEGIN
119
+ INSERT INTO ${prefix}entries_fts(${prefix}entries_fts, rowid, title, body, tags)
120
+ VALUES ('delete', old.rowid, old.title, old.body, old.tags);
121
+ END;
122
+ CREATE TRIGGER ${prefix}entries_au AFTER UPDATE ON ${prefix}entries BEGIN
123
+ INSERT INTO ${prefix}entries_fts(${prefix}entries_fts, rowid, title, body, tags)
124
+ VALUES ('delete', old.rowid, old.title, old.body, old.tags);
125
+ INSERT INTO ${prefix}entries_fts(rowid, title, body, tags)
126
+ VALUES (new.rowid, new.title, new.body, new.tags);
127
+ END;
128
+ `);
129
+ });
130
+ }
131
+ }
132
+ ];
133
+ for (let i = 1; i < MIGRATIONS.length; i++) {
134
+ if (MIGRATIONS[i].version <= MIGRATIONS[i - 1].version) {
135
+ throw new Error(
136
+ `migrations.ts: MIGRATIONS must be in strictly ascending version order. Found version ${MIGRATIONS[i].version} after ${MIGRATIONS[i - 1].version} at index ${i}.`
137
+ );
138
+ }
139
+ }
140
+ var CURRENT_SCHEMA_VERSION = MIGRATIONS.length > 0 ? MIGRATIONS[MIGRATIONS.length - 1].version : 0;
141
+
87
142
  // src/types.ts
88
143
  var WikiBusyError = class extends Error {
89
144
  operation;
@@ -344,45 +399,54 @@ var WikiMemory = class {
344
399
  this.prefix = options.config?.tablePrefix || "llm_wiki_";
345
400
  }
346
401
  async setup() {
347
- await setupDatabase(this.db, this.prefix);
348
- const ftsMeta = await this.db.getFirstAsync(
349
- `SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
350
- [`${this.prefix}entries_fts`]
402
+ const entriesExistedBeforeSetup = await this.db.getFirstAsync(
403
+ `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
404
+ [`${this.prefix}entries`]
351
405
  );
352
- const hasPorterTokenizer = /tokenize\s*=\s*['"]porter\s+unicode61['"]/i.test(ftsMeta?.sql ?? "");
353
- if (ftsMeta?.sql && !hasPorterTokenizer) {
354
- await this.db.withTransactionAsync(async () => {
355
- await this.db.execAsync(`
356
- DROP TRIGGER IF EXISTS ${this.prefix}entries_ai;
357
- DROP TRIGGER IF EXISTS ${this.prefix}entries_ad;
358
- DROP TRIGGER IF EXISTS ${this.prefix}entries_au;
359
- DROP TABLE IF EXISTS ${this.prefix}entries_fts;
360
- CREATE VIRTUAL TABLE ${this.prefix}entries_fts USING fts5(
361
- title,
362
- body,
363
- tags,
364
- content='${this.prefix}entries',
365
- content_rowid='rowid',
366
- tokenize='porter unicode61'
367
- );
368
- INSERT INTO ${this.prefix}entries_fts(rowid, title, body, tags)
369
- SELECT rowid, title, body, tags FROM ${this.prefix}entries;
370
- CREATE TRIGGER ${this.prefix}entries_ai AFTER INSERT ON ${this.prefix}entries BEGIN
371
- INSERT INTO ${this.prefix}entries_fts(rowid, title, body, tags)
372
- VALUES (new.rowid, new.title, new.body, new.tags);
373
- END;
374
- CREATE TRIGGER ${this.prefix}entries_ad AFTER DELETE ON ${this.prefix}entries BEGIN
375
- INSERT INTO ${this.prefix}entries_fts(${this.prefix}entries_fts, rowid, title, body, tags)
376
- VALUES ('delete', old.rowid, old.title, old.body, old.tags);
377
- END;
378
- CREATE TRIGGER ${this.prefix}entries_au AFTER UPDATE ON ${this.prefix}entries BEGIN
379
- INSERT INTO ${this.prefix}entries_fts(${this.prefix}entries_fts, rowid, title, body, tags)
380
- VALUES ('delete', old.rowid, old.title, old.body, old.tags);
381
- INSERT INTO ${this.prefix}entries_fts(rowid, title, body, tags)
382
- VALUES (new.rowid, new.title, new.body, new.tags);
383
- END;
384
- `);
385
- });
406
+ await setupDatabase(this.db, this.prefix);
407
+ let currentVersion;
408
+ if (!entriesExistedBeforeSetup) {
409
+ await this.db.runAsync(
410
+ `INSERT OR REPLACE INTO ${this.prefix}meta (key, value) VALUES ('schema_version', ?)`,
411
+ [String(CURRENT_SCHEMA_VERSION)]
412
+ );
413
+ currentVersion = CURRENT_SCHEMA_VERSION;
414
+ } else {
415
+ const metaRow = await this.db.getFirstAsync(
416
+ `SELECT value FROM ${this.prefix}meta WHERE key = 'schema_version'`
417
+ );
418
+ if (metaRow) {
419
+ currentVersion = parseInt(metaRow.value, 10);
420
+ if (!Number.isFinite(currentVersion)) currentVersion = 0;
421
+ } else {
422
+ const ftsMeta = await this.db.getFirstAsync(
423
+ `SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
424
+ [`${this.prefix}entries_fts`]
425
+ );
426
+ const hasPorter = /tokenize\s*=\s*['"]porter\s+unicode61['"]/i.test(ftsMeta?.sql ?? "");
427
+ currentVersion = hasPorter ? 1 : 0;
428
+ }
429
+ }
430
+ for (const migration of MIGRATIONS) {
431
+ if (migration.version > currentVersion) {
432
+ await migration.run(this.db, this.prefix);
433
+ await this.db.runAsync(
434
+ `INSERT OR REPLACE INTO ${this.prefix}meta (key, value) VALUES ('schema_version', ?)`,
435
+ [String(migration.version)]
436
+ );
437
+ currentVersion = migration.version;
438
+ }
439
+ }
440
+ if (entriesExistedBeforeSetup) {
441
+ const metaCheck = await this.db.getFirstAsync(
442
+ `SELECT value FROM ${this.prefix}meta WHERE key = 'schema_version'`
443
+ );
444
+ if (!metaCheck) {
445
+ await this.db.runAsync(
446
+ `INSERT OR REPLACE INTO ${this.prefix}meta (key, value) VALUES ('schema_version', ?)`,
447
+ [String(currentVersion)]
448
+ );
449
+ }
386
450
  }
387
451
  const rows = await this.db.getAllAsync(`
388
452
  SELECT rowid, source_ref FROM ${this.prefix}entries
@@ -407,6 +471,101 @@ var WikiMemory = class {
407
471
  }
408
472
  });
409
473
  }
474
+ async hasChanged(entityId, sourceRef, sourceHash) {
475
+ const normalizedRef = normalizeSourceRef(sourceRef);
476
+ if (!normalizedRef) {
477
+ throw new Error(`Invalid sourceRef: "${sourceRef}"`);
478
+ }
479
+ const normalizedHash = normalizeSourceHash(sourceHash);
480
+ if (!normalizedHash) {
481
+ throw new Error(`Invalid sourceHash: must be a 64-character hex string (normalized to lowercase)`);
482
+ }
483
+ const row = await this.db.getFirstAsync(
484
+ `SELECT source_hash FROM ${this.prefix}entries
485
+ WHERE entity_id = ? AND source_ref = ? AND deleted_at IS NULL
486
+ ORDER BY updated_at DESC
487
+ LIMIT 1`,
488
+ [entityId, normalizedRef]
489
+ );
490
+ if (!row) return true;
491
+ const normalizedStoredHash = row.source_hash ? normalizeSourceHash(row.source_hash) : null;
492
+ return normalizedStoredHash !== normalizedHash;
493
+ }
494
+ _pruneKey(entityId) {
495
+ return `${this.prefix}:${entityId}:prune`;
496
+ }
497
+ _validatePruneDuration(value, name) {
498
+ if (value !== null && value !== void 0 && (typeof value !== "number" || !isFinite(value) || value < 0)) {
499
+ throw new Error(`Invalid ${name}: must be a non-negative finite number or null`);
500
+ }
501
+ }
502
+ async runPrune(entityId, options) {
503
+ const pruneKey = this._pruneKey(entityId);
504
+ const ingestPrefix = `${this.prefix}:${entityId}:`;
505
+ let isIngestRunning = false;
506
+ for (const k of this.activeIngestJobs) {
507
+ if (k.startsWith(ingestPrefix)) {
508
+ isIngestRunning = true;
509
+ break;
510
+ }
511
+ }
512
+ let blockingOperation = null;
513
+ if (this.activeMaintenanceJobs.has(pruneKey)) {
514
+ blockingOperation = "prune";
515
+ } else if (this.activeMaintenanceJobs.has(this._librarianKey(entityId))) {
516
+ blockingOperation = "librarian";
517
+ } else if (this.activeMaintenanceJobs.has(this._healKey(entityId))) {
518
+ blockingOperation = "heal";
519
+ } else if (isIngestRunning) {
520
+ blockingOperation = "ingest";
521
+ }
522
+ if (blockingOperation !== null) {
523
+ throw new WikiBusyError(blockingOperation, entityId);
524
+ }
525
+ this.activeMaintenanceJobs.add(pruneKey);
526
+ try {
527
+ const retainSoftDeletedFor = options?.retainSoftDeletedFor !== void 0 ? options.retainSoftDeletedFor : this.options.config?.pruneRetainSoftDeletedFor ?? 7;
528
+ const retainEventsFor = options?.retainEventsFor !== void 0 ? options.retainEventsFor : this.options.config?.pruneEventsAfter ?? 30;
529
+ const vacuum = options?.vacuum ?? false;
530
+ this._validatePruneDuration(retainSoftDeletedFor, "retainSoftDeletedFor");
531
+ this._validatePruneDuration(retainEventsFor, "retainEventsFor");
532
+ const now = Date.now();
533
+ let deletedEntries = 0;
534
+ let deletedTasks = 0;
535
+ let deletedEvents = 0;
536
+ if (retainSoftDeletedFor !== null) {
537
+ const cutoff = now - retainSoftDeletedFor * 864e5;
538
+ const entryResult = await this.db.runAsync(
539
+ `DELETE FROM ${this.prefix}entries
540
+ WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
541
+ [entityId, cutoff]
542
+ );
543
+ deletedEntries = entryResult.changes;
544
+ const taskResult = await this.db.runAsync(
545
+ `DELETE FROM ${this.prefix}tasks
546
+ WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at < ?`,
547
+ [entityId, cutoff]
548
+ );
549
+ deletedTasks = taskResult.changes;
550
+ }
551
+ if (retainEventsFor !== null) {
552
+ const cutoff = now - retainEventsFor * 864e5;
553
+ const eventResult = await this.db.runAsync(
554
+ `DELETE FROM ${this.prefix}events
555
+ WHERE entity_id = ? AND created_at < ?`,
556
+ [entityId, cutoff]
557
+ );
558
+ deletedEvents = eventResult.changes;
559
+ }
560
+ if (vacuum) {
561
+ await this.db.execAsync(`PRAGMA wal_checkpoint(TRUNCATE)`);
562
+ await this.db.execAsync(`VACUUM`);
563
+ }
564
+ return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
565
+ } finally {
566
+ this.activeMaintenanceJobs.delete(pruneKey);
567
+ }
568
+ }
410
569
  formatSearchQuery(query) {
411
570
  const normalizeTokens = (value) => value.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((t) => t.length >= 3);
412
571
  const baseTokens = normalizeTokens(query);
@@ -512,7 +671,7 @@ var WikiMemory = class {
512
671
  if (memoryCheckpoint > count) memoryCheckpoint = 0;
513
672
  if (count - memoryCheckpoint >= threshold) {
514
673
  const jobKey = this._librarianKey(entityId);
515
- if (!this.activeMaintenanceJobs.has(jobKey)) {
674
+ if (!this.activeMaintenanceJobs.has(jobKey) && !this.activeMaintenanceJobs.has(this._pruneKey(entityId))) {
516
675
  this.activeMaintenanceJobs.add(jobKey);
517
676
  this.runLibrarianThenMaybeHeal(entityId, count).catch(console.error).finally(() => this.activeMaintenanceJobs.delete(jobKey));
518
677
  }
@@ -690,6 +849,9 @@ The following document anchors are provided for contradiction detection only. Do
690
849
  if (this.activeMaintenanceJobs.has(jobKey)) {
691
850
  throw new WikiBusyError("librarian", entityId);
692
851
  }
852
+ if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) {
853
+ throw new WikiBusyError("prune", entityId);
854
+ }
693
855
  this.activeMaintenanceJobs.add(jobKey);
694
856
  try {
695
857
  await this._doRunLibrarian(entityId);
@@ -702,6 +864,9 @@ The following document anchors are provided for contradiction detection only. Do
702
864
  if (this.activeMaintenanceJobs.has(jobKey)) {
703
865
  throw new WikiBusyError("heal", entityId);
704
866
  }
867
+ if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) {
868
+ throw new WikiBusyError("prune", entityId);
869
+ }
705
870
  this.activeMaintenanceJobs.add(jobKey);
706
871
  try {
707
872
  await this._doRunHeal(entityId);
@@ -952,6 +1117,9 @@ The following document anchors are provided for contradiction detection only. Do
952
1117
  if (this.activeIngestJobs.has(jobKey)) {
953
1118
  throw new WikiBusyError("ingest", entityId);
954
1119
  }
1120
+ if (this.activeMaintenanceJobs.has(this._pruneKey(entityId))) {
1121
+ throw new WikiBusyError("prune", entityId);
1122
+ }
955
1123
  this.activeIngestJobs.add(jobKey);
956
1124
  try {
957
1125
  const { chunks, truncated } = chunkText(params.documentChunk, maxChunkLength, chunkOverlap);
@@ -1089,6 +1257,121 @@ function formatMemoryDump(dump) {
1089
1257
  };
1090
1258
  }
1091
1259
 
1260
+ // src/utils/formatContext.ts
1261
+ function validateMaxOption(value, name) {
1262
+ if (!isFinite(value) || value < 0) {
1263
+ throw new Error(`Invalid ${name}: must be a non-negative finite number`);
1264
+ }
1265
+ }
1266
+ var CONFIDENCE_WEIGHT = {
1267
+ certain: 1,
1268
+ inferred: 0.6,
1269
+ tentative: 0.3
1270
+ };
1271
+ function scoreFactFor(fact, weights, now) {
1272
+ const confW = CONFIDENCE_WEIGHT[fact.confidence] ?? 0.3;
1273
+ const ageDays = (now - fact.updated_at) / 864e5;
1274
+ const recencyDecay = Math.exp(-ageDays / 30);
1275
+ return confW * weights.confidence + Math.log(1 + fact.access_count) * weights.accessCount + recencyDecay * weights.recency;
1276
+ }
1277
+ function renderFactMarkdown(fact, includeConfidence, includeTags) {
1278
+ const confPart = includeConfidence ? ` (${fact.confidence})` : "";
1279
+ const tagPart = includeTags && fact.tags.length > 0 ? ` [${fact.tags.join(", ")}]` : "";
1280
+ return `- **${fact.title}**${confPart}${tagPart}
1281
+ ${fact.body.replace(/\n/g, "\n ")}`;
1282
+ }
1283
+ function renderFactPlain(fact, includeConfidence, includeTags) {
1284
+ const confPart = includeConfidence ? ` (${fact.confidence})` : "";
1285
+ const tagPart = includeTags && fact.tags.length > 0 ? ` [${fact.tags.join(", ")}]` : "";
1286
+ return `${fact.title}${confPart}${tagPart}: ${fact.body}`;
1287
+ }
1288
+ function renderTaskMarkdown(task) {
1289
+ return `- [P${task.priority}] ${task.description.replace(/\n/g, "\n ")} (${task.status})`;
1290
+ }
1291
+ function renderTaskPlain(task) {
1292
+ return `[P${task.priority}] ${task.description} (${task.status})`;
1293
+ }
1294
+ function renderEventMarkdown(event) {
1295
+ const ts = new Date(event.created_at).toISOString();
1296
+ return `- [${event.event_type} @ ${ts}] ${event.summary.replace(/\n/g, "\n ")}`;
1297
+ }
1298
+ function renderEventPlain(event) {
1299
+ const ts = new Date(event.created_at).toISOString();
1300
+ return `[${event.event_type} @ ${ts}] ${event.summary}`;
1301
+ }
1302
+ function formatContext(bundle, options) {
1303
+ const opts = {
1304
+ format: options?.format ?? "markdown",
1305
+ maxFacts: options?.maxFacts ?? 10,
1306
+ maxTasks: options?.maxTasks ?? 10,
1307
+ maxEvents: options?.maxEvents ?? 10,
1308
+ includeConfidence: options?.includeConfidence ?? true,
1309
+ includeTags: options?.includeTags ?? true,
1310
+ factWeights: {
1311
+ confidence: options?.factWeights?.confidence ?? 1,
1312
+ accessCount: options?.factWeights?.accessCount ?? 0.3,
1313
+ recency: options?.factWeights?.recency ?? 0.5
1314
+ }
1315
+ };
1316
+ validateMaxOption(opts.maxFacts, "maxFacts");
1317
+ validateMaxOption(opts.maxTasks, "maxTasks");
1318
+ validateMaxOption(opts.maxEvents, "maxEvents");
1319
+ const weights = opts.factWeights;
1320
+ const now = Date.now();
1321
+ const sortedFacts = [...bundle.facts].sort((a, b) => scoreFactFor(b, weights, now) - scoreFactFor(a, weights, now)).slice(0, opts.maxFacts);
1322
+ const sortedTasks = [...bundle.tasks].sort((a, b) => b.priority - a.priority || a.created_at - b.created_at).slice(0, opts.maxTasks);
1323
+ const sortedEvents = [...bundle.events].sort((a, b) => b.created_at - a.created_at).slice(0, opts.maxEvents);
1324
+ if (sortedFacts.length === 0 && sortedTasks.length === 0 && sortedEvents.length === 0) {
1325
+ return "";
1326
+ }
1327
+ const isMarkdown = opts.format === "markdown";
1328
+ const lines = [];
1329
+ if (isMarkdown) {
1330
+ lines.push("## Memory");
1331
+ if (sortedFacts.length > 0) {
1332
+ lines.push("");
1333
+ lines.push("### Known Facts");
1334
+ for (const fact of sortedFacts) {
1335
+ lines.push(renderFactMarkdown(fact, opts.includeConfidence, opts.includeTags));
1336
+ }
1337
+ }
1338
+ if (sortedTasks.length > 0) {
1339
+ lines.push("");
1340
+ lines.push("### Open Tasks");
1341
+ for (const task of sortedTasks) {
1342
+ lines.push(renderTaskMarkdown(task));
1343
+ }
1344
+ }
1345
+ if (sortedEvents.length > 0) {
1346
+ lines.push("");
1347
+ lines.push("### Recent Events");
1348
+ for (const event of sortedEvents) {
1349
+ lines.push(renderEventMarkdown(event));
1350
+ }
1351
+ }
1352
+ } else {
1353
+ if (sortedFacts.length > 0) {
1354
+ lines.push("KNOWN FACTS:");
1355
+ for (const fact of sortedFacts) {
1356
+ lines.push(renderFactPlain(fact, opts.includeConfidence, opts.includeTags));
1357
+ }
1358
+ }
1359
+ if (sortedTasks.length > 0) {
1360
+ lines.push("OPEN TASKS:");
1361
+ for (const task of sortedTasks) {
1362
+ lines.push(renderTaskPlain(task));
1363
+ }
1364
+ }
1365
+ if (sortedEvents.length > 0) {
1366
+ lines.push("RECENT EVENTS:");
1367
+ for (const event of sortedEvents) {
1368
+ lines.push(renderEventPlain(event));
1369
+ }
1370
+ }
1371
+ }
1372
+ return lines.join("\n");
1373
+ }
1374
+
1092
1375
  // src/index.ts
1093
1376
  function createWiki(db, options) {
1094
1377
  return new WikiMemory(db, options);
@@ -1097,5 +1380,6 @@ export {
1097
1380
  WikiBusyError,
1098
1381
  WikiMemory,
1099
1382
  createWiki,
1383
+ formatContext,
1100
1384
  formatMemoryDump
1101
1385
  };
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { a as WikiMemory, e as MemoryBundle, i as WikiEvent, M as MemoryDump } from '../WikiMemory-CjlQ68X0.mjs';
3
+ import { c as WikiMemory, a as MemoryBundle, i as WikiEvent, M as MemoryDump } from '../WikiMemory-ChQmVyvA.mjs';
4
4
  import 'expo-sqlite';
5
5
 
6
6
  declare function WikiProvider({ wiki, children }: {
@@ -24,10 +24,30 @@ declare function useWikiWrite(): {
24
24
  error: Error | null;
25
25
  };
26
26
 
27
+ type MaintenanceResult = {
28
+ operation: 'librarian' | 'heal';
29
+ result: undefined;
30
+ } | {
31
+ operation: 'prune';
32
+ result: {
33
+ entries: number;
34
+ tasks: number;
35
+ events: number;
36
+ };
37
+ };
27
38
  declare function useWikiMaintenance(): {
28
39
  runLibrarian: (entityId: string) => Promise<void>;
29
40
  runHeal: (entityId: string) => Promise<void>;
30
- lastResult: void | null;
41
+ runPrune: (entityId: string, options?: {
42
+ retainSoftDeletedFor?: number | null;
43
+ retainEventsFor?: number | null;
44
+ vacuum?: boolean;
45
+ }) => Promise<{
46
+ entries: number;
47
+ tasks: number;
48
+ events: number;
49
+ }>;
50
+ lastResult: MaintenanceResult | null;
31
51
  isPending: boolean;
32
52
  error: Error | null;
33
53
  };
@@ -76,4 +96,11 @@ declare function useWikiExport(): {
76
96
  error: Error | null;
77
97
  };
78
98
 
79
- export { WikiProvider, useMemoryRead, useWiki, useWikiExport, useWikiForget, useWikiIngest, useWikiMaintenance, useWikiWrite };
99
+ declare function useWikiHasChanged(): {
100
+ execute: (entityId: string, sourceRef: string, sourceHash: string) => Promise<boolean>;
101
+ lastResult: boolean | null;
102
+ isPending: boolean;
103
+ error: Error | null;
104
+ };
105
+
106
+ export { type MaintenanceResult, WikiProvider, useMemoryRead, useWiki, useWikiExport, useWikiForget, useWikiHasChanged, useWikiIngest, useWikiMaintenance, useWikiWrite };
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { a as WikiMemory, e as MemoryBundle, i as WikiEvent, M as MemoryDump } from '../WikiMemory-CjlQ68X0.js';
3
+ import { c as WikiMemory, a as MemoryBundle, i as WikiEvent, M as MemoryDump } from '../WikiMemory-ChQmVyvA.js';
4
4
  import 'expo-sqlite';
5
5
 
6
6
  declare function WikiProvider({ wiki, children }: {
@@ -24,10 +24,30 @@ declare function useWikiWrite(): {
24
24
  error: Error | null;
25
25
  };
26
26
 
27
+ type MaintenanceResult = {
28
+ operation: 'librarian' | 'heal';
29
+ result: undefined;
30
+ } | {
31
+ operation: 'prune';
32
+ result: {
33
+ entries: number;
34
+ tasks: number;
35
+ events: number;
36
+ };
37
+ };
27
38
  declare function useWikiMaintenance(): {
28
39
  runLibrarian: (entityId: string) => Promise<void>;
29
40
  runHeal: (entityId: string) => Promise<void>;
30
- lastResult: void | null;
41
+ runPrune: (entityId: string, options?: {
42
+ retainSoftDeletedFor?: number | null;
43
+ retainEventsFor?: number | null;
44
+ vacuum?: boolean;
45
+ }) => Promise<{
46
+ entries: number;
47
+ tasks: number;
48
+ events: number;
49
+ }>;
50
+ lastResult: MaintenanceResult | null;
31
51
  isPending: boolean;
32
52
  error: Error | null;
33
53
  };
@@ -76,4 +96,11 @@ declare function useWikiExport(): {
76
96
  error: Error | null;
77
97
  };
78
98
 
79
- export { WikiProvider, useMemoryRead, useWiki, useWikiExport, useWikiForget, useWikiIngest, useWikiMaintenance, useWikiWrite };
99
+ declare function useWikiHasChanged(): {
100
+ execute: (entityId: string, sourceRef: string, sourceHash: string) => Promise<boolean>;
101
+ lastResult: boolean | null;
102
+ isPending: boolean;
103
+ error: Error | null;
104
+ };
105
+
106
+ export { type MaintenanceResult, WikiProvider, useMemoryRead, useWiki, useWikiExport, useWikiForget, useWikiHasChanged, useWikiIngest, useWikiMaintenance, useWikiWrite };
@@ -25,6 +25,7 @@ __export(react_exports, {
25
25
  useWiki: () => useWiki,
26
26
  useWikiExport: () => useWikiExport,
27
27
  useWikiForget: () => useWikiForget,
28
+ useWikiHasChanged: () => useWikiHasChanged,
28
29
  useWikiIngest: () => useWikiIngest,
29
30
  useWikiMaintenance: () => useWikiMaintenance,
30
31
  useWikiWrite: () => useWikiWrite
@@ -134,7 +135,7 @@ function useWikiMaintenance() {
134
135
  setLastResult(null);
135
136
  try {
136
137
  await wikiRef.current.runLibrarian(entityId);
137
- setLastResult(void 0);
138
+ setLastResult({ operation: "librarian", result: void 0 });
138
139
  } catch (e) {
139
140
  const err = e instanceof Error ? e : new Error(String(e));
140
141
  setError(err);
@@ -151,7 +152,7 @@ function useWikiMaintenance() {
151
152
  setLastResult(null);
152
153
  try {
153
154
  await wikiRef.current.runHeal(entityId);
154
- setLastResult(void 0);
155
+ setLastResult({ operation: "heal", result: void 0 });
155
156
  } catch (e) {
156
157
  const err = e instanceof Error ? e : new Error(String(e));
157
158
  setError(err);
@@ -161,7 +162,28 @@ function useWikiMaintenance() {
161
162
  if (pendingCount.current === 0) setIsPending(false);
162
163
  }
163
164
  }, []);
164
- return { runLibrarian, runHeal, lastResult, isPending, error };
165
+ const runPrune = (0, import_react4.useCallback)(
166
+ async (entityId, options) => {
167
+ setError(null);
168
+ pendingCount.current += 1;
169
+ setIsPending(true);
170
+ setLastResult(null);
171
+ try {
172
+ const result = await wikiRef.current.runPrune(entityId, options);
173
+ setLastResult({ operation: "prune", result });
174
+ return result;
175
+ } catch (e) {
176
+ const err = e instanceof Error ? e : new Error(String(e));
177
+ setError(err);
178
+ throw err;
179
+ } finally {
180
+ pendingCount.current -= 1;
181
+ if (pendingCount.current === 0) setIsPending(false);
182
+ }
183
+ },
184
+ []
185
+ );
186
+ return { runLibrarian, runHeal, runPrune, lastResult, isPending, error };
165
187
  }
166
188
 
167
189
  // src/react/useWikiIngest.ts
@@ -247,6 +269,37 @@ function useWikiExport() {
247
269
  }, []);
248
270
  return { execute, lastResult, isPending, error };
249
271
  }
272
+
273
+ // src/react/useWikiHasChanged.ts
274
+ var import_react8 = require("react");
275
+ function useWikiHasChanged() {
276
+ const wiki = useWiki();
277
+ const wikiRef = (0, import_react8.useRef)(wiki);
278
+ wikiRef.current = wiki;
279
+ const [isPending, setIsPending] = (0, import_react8.useState)(false);
280
+ const [error, setError] = (0, import_react8.useState)(null);
281
+ const [lastResult, setLastResult] = (0, import_react8.useState)(null);
282
+ const execute = (0, import_react8.useCallback)(
283
+ async (entityId, sourceRef, sourceHash) => {
284
+ setError(null);
285
+ setIsPending(true);
286
+ setLastResult(null);
287
+ try {
288
+ const result = await wikiRef.current.hasChanged(entityId, sourceRef, sourceHash);
289
+ setLastResult(result);
290
+ return result;
291
+ } catch (e) {
292
+ const err = e instanceof Error ? e : new Error(String(e));
293
+ setError(err);
294
+ throw err;
295
+ } finally {
296
+ setIsPending(false);
297
+ }
298
+ },
299
+ []
300
+ );
301
+ return { execute, lastResult, isPending, error };
302
+ }
250
303
  // Annotate the CommonJS export names for ESM import in node:
251
304
  0 && (module.exports = {
252
305
  WikiProvider,
@@ -254,6 +307,7 @@ function useWikiExport() {
254
307
  useWiki,
255
308
  useWikiExport,
256
309
  useWikiForget,
310
+ useWikiHasChanged,
257
311
  useWikiIngest,
258
312
  useWikiMaintenance,
259
313
  useWikiWrite