@equationalapplications/core-llm-wiki 4.6.0 → 4.6.1

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
@@ -139,6 +139,157 @@ var PrunePartialFailureError = class extends Error {
139
139
  }
140
140
  };
141
141
 
142
+ // src/repositories/BaseRepository.ts
143
+ var BaseRepository = class {
144
+ constructor(db, prefix) {
145
+ this.db = db;
146
+ this.prefix = prefix;
147
+ }
148
+ /**
149
+ * Return the DB executor for a given transaction handle.
150
+ * If tx is provided, use it; otherwise fall back to this.db.
151
+ */
152
+ getExecutor(tx) {
153
+ return tx ?? this.db;
154
+ }
155
+ };
156
+
157
+ // src/repositories/EntryRepository.ts
158
+ function mapRowToFact(row) {
159
+ const tags = (() => {
160
+ if (Array.isArray(row.tags)) return row.tags;
161
+ try {
162
+ const p = JSON.parse(row.tags);
163
+ if (Array.isArray(p)) return p;
164
+ } catch {
165
+ }
166
+ return [];
167
+ })();
168
+ return {
169
+ id: row.id,
170
+ entity_id: row.entity_id,
171
+ title: row.title,
172
+ body: row.body,
173
+ tags,
174
+ confidence: row.confidence,
175
+ source_type: row.source_type,
176
+ source_hash: row.source_hash ?? null,
177
+ source_ref: row.source_ref ?? null,
178
+ created_at: Number(row.created_at),
179
+ updated_at: Number(row.updated_at),
180
+ last_accessed_at: row.last_accessed_at === null || row.last_accessed_at === void 0 ? null : Number(row.last_accessed_at),
181
+ deleted_at: row.deleted_at != null ? Number(row.deleted_at) : null,
182
+ access_count: Number(row.access_count ?? 0)
183
+ };
184
+ }
185
+ var EntryRepository = class extends BaseRepository {
186
+ constructor() {
187
+ super(...arguments);
188
+ this.chunkSize = 500;
189
+ }
190
+ /**
191
+ * Fetch facts by IDs, optionally scoped to entity IDs.
192
+ * Returns facts in the order of the input IDs (first match wins).
193
+ */
194
+ async findByIds(ids, scopedEntityIds, tx) {
195
+ const executor = this.getExecutor(tx);
196
+ const rows = [];
197
+ const entityClause = scopedEntityIds && scopedEntityIds.length > 0 ? ` AND entity_id IN (${scopedEntityIds.map(() => "?").join(",")})` : "";
198
+ const entityParams = scopedEntityIds ?? [];
199
+ for (let i = 0; i < ids.length; i += this.chunkSize) {
200
+ const chunk = ids.slice(i, i + this.chunkSize);
201
+ const placeholders = chunk.map(() => "?").join(",");
202
+ const chunkRows = await executor.getAllAsync(
203
+ `SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders})${entityClause} AND deleted_at IS NULL`,
204
+ [...chunk, ...entityParams]
205
+ );
206
+ rows.push(...chunkRows);
207
+ }
208
+ const byId = new Map(rows.map((r) => [r.id, r]));
209
+ return ids.map((id) => byId.get(id)).filter((r) => r !== void 0).map(mapRowToFact);
210
+ }
211
+ /**
212
+ * Upsert a WikiFact. Nullable fields set to null when fact value is null.
213
+ * Returns { changes, lastInsertRowId }.
214
+ */
215
+ async upsert(fact, tx) {
216
+ const executor = this.getExecutor(tx);
217
+ const now = Date.now();
218
+ const tagsJson = JSON.stringify(fact.tags);
219
+ const embeddingBlob = fact.embedding_blob instanceof Uint8Array ? fact.embedding_blob : fact.embedding_blob && typeof fact.embedding_blob === "object" && "type" in fact.embedding_blob ? new Uint8Array(fact.embedding_blob.data) : fact.embedding_blob && typeof fact.embedding_blob === "object" ? (() => {
220
+ const obj = fact.embedding_blob;
221
+ const keys = Object.keys(obj).map(Number).sort((a, b) => a - b);
222
+ const arr = new Uint8Array(keys.length);
223
+ for (let i = 0; i < keys.length; i++) arr[i] = obj[String(keys[i])];
224
+ return arr;
225
+ })() : void 0;
226
+ return executor.runAsync(
227
+ `INSERT INTO ${this.prefix}entries (
228
+ id, entity_id, title, body, tags, confidence, source_type,
229
+ source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count,
230
+ deleted_at, embedding_blob, embedding
231
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
232
+ ON CONFLICT(id) DO UPDATE SET
233
+ entity_id = excluded.entity_id,
234
+ title = excluded.title,
235
+ body = excluded.body,
236
+ tags = excluded.tags,
237
+ confidence = excluded.confidence,
238
+ source_type = excluded.source_type,
239
+ source_hash = excluded.source_hash,
240
+ source_ref = excluded.source_ref,
241
+ updated_at = excluded.updated_at,
242
+ last_accessed_at = excluded.last_accessed_at,
243
+ access_count = excluded.access_count,
244
+ deleted_at = excluded.deleted_at,
245
+ embedding_blob = CASE WHEN excluded.embedding_blob IS NULL THEN embedding_blob ELSE excluded.embedding_blob END,
246
+ embedding = NULL`,
247
+ [
248
+ fact.id,
249
+ fact.entity_id,
250
+ fact.title,
251
+ fact.body,
252
+ tagsJson,
253
+ fact.confidence,
254
+ fact.source_type,
255
+ fact.source_hash,
256
+ fact.source_ref,
257
+ fact.created_at,
258
+ now,
259
+ // updated_at set by repo
260
+ fact.last_accessed_at === null ? null : fact.last_accessed_at,
261
+ fact.access_count,
262
+ fact.deleted_at ?? null,
263
+ embeddingBlob ?? null,
264
+ null
265
+ ]
266
+ );
267
+ }
268
+ /**
269
+ * Soft-delete a single entry by ID scoped to entityId. Sets deleted_at + updated_at.
270
+ */
271
+ async softDelete(entryId, entityId, tx) {
272
+ const executor = this.getExecutor(tx);
273
+ const now = Date.now();
274
+ return executor.runAsync(
275
+ `UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`,
276
+ [now, now, entryId, entityId]
277
+ );
278
+ }
279
+ /**
280
+ * Fetch IDs + entity_ids of soft-deleted rows older than cutoff for a given entity.
281
+ * Used by runPrune().
282
+ */
283
+ async getPrunableMetadata(entityId, cutoff, tx) {
284
+ const executor = this.getExecutor(tx);
285
+ return executor.getAllAsync(
286
+ `SELECT id, entity_id FROM ${this.prefix}entries
287
+ WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
288
+ [entityId, cutoff]
289
+ );
290
+ }
291
+ };
292
+
142
293
  // src/prompts.ts
143
294
  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.
144
295
  Return ONLY a valid JSON object matching this schema:
@@ -461,6 +612,7 @@ var _WikiMemory = class _WikiMemory {
461
612
  this.db = db;
462
613
  this.options = options;
463
614
  this.prefix = options.config?.tablePrefix || "llm_wiki_";
615
+ this.entryRepo = new EntryRepository(db, this.prefix);
464
616
  }
465
617
  normalizeMiniSearchRow(row) {
466
618
  return {
@@ -916,11 +1068,7 @@ After running the migration SQL, restart your application.`
916
1068
  let deletedEvents = 0;
917
1069
  if (retainSoftDeletedFor !== null) {
918
1070
  const cutoff = now - retainSoftDeletedFor * 864e5;
919
- const entriesToDelete = await this.db.getAllAsync(
920
- `SELECT id, entity_id FROM ${this.prefix}entries
921
- WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
922
- [entityId, cutoff]
923
- );
1071
+ const entriesToDelete = await this.entryRepo.getPrunableMetadata(entityId, cutoff);
924
1072
  const succeeded = [];
925
1073
  let failure = null;
926
1074
  for (const row of entriesToDelete) {
@@ -1476,7 +1624,15 @@ After running the migration SQL, restart your application.`
1476
1624
  const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
1477
1625
  return {
1478
1626
  ...rest,
1479
- tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
1627
+ tags: (() => {
1628
+ if (Array.isArray(rest.tags)) return rest.tags;
1629
+ try {
1630
+ const p = JSON.parse(rest.tags);
1631
+ return Array.isArray(p) ? p : [];
1632
+ } catch {
1633
+ return [];
1634
+ }
1635
+ })()
1480
1636
  };
1481
1637
  });
1482
1638
  }
@@ -1556,28 +1712,8 @@ After running the migration SQL, restart your application.`
1556
1712
  * Hydrate full facts by ID. Pass scopedEntityIds to restrict to requested namespaces in SQL
1557
1713
  * (defense-in-depth against a rogue VectorRanker returning cross-entity IDs).
1558
1714
  */
1559
- async _hydrateFactsByIds(ids, scopedEntityIds) {
1560
- const fullRows = [];
1561
- const chunkSize = 500;
1562
- const entityClause = scopedEntityIds && scopedEntityIds.length > 0 ? ` AND entity_id IN (${scopedEntityIds.map(() => "?").join(",")})` : "";
1563
- const entityParams = scopedEntityIds && scopedEntityIds.length > 0 ? [...scopedEntityIds] : [];
1564
- for (let i = 0; i < ids.length; i += chunkSize) {
1565
- const idChunk = ids.slice(i, i + chunkSize);
1566
- const placeholders = idChunk.map(() => "?").join(",");
1567
- const chunkRows = await this.db.getAllAsync(
1568
- `SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders})${entityClause} AND deleted_at IS NULL`,
1569
- [...idChunk, ...entityParams]
1570
- );
1571
- fullRows.push(...chunkRows);
1572
- }
1573
- const byId = new Map(fullRows.map((row) => [row.id, row]));
1574
- return ids.map((id) => byId.get(id)).filter((fact) => fact !== void 0).map((fact) => {
1575
- const { embedding: _embedding, embedding_blob: _blob, ...rest } = fact;
1576
- return {
1577
- ...rest,
1578
- tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
1579
- };
1580
- });
1715
+ async _hydrateFactsByIds(ids, scopedEntityIds, tx) {
1716
+ return this.entryRepo.findByIds(ids, scopedEntityIds, tx);
1581
1717
  }
1582
1718
  /**
1583
1719
  * Strip potentially sensitive data from ranker errors before exposing to host callbacks.
@@ -2590,7 +2726,7 @@ The following document anchors are provided for contradiction detection only. Do
2590
2726
  const entriesToDelete = await this.db.getAllAsync(q, args);
2591
2727
  deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
2592
2728
  }
2593
- 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;
2729
+ const entryPromise = params.entryId ? this.entryRepo.softDelete(params.entryId, entityId) : null;
2594
2730
  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;
2595
2731
  let refPromise = null;
2596
2732
  if (sourceRef || sourceHash) {
@@ -2727,11 +2863,23 @@ ${chunk}`;
2727
2863
  );
2728
2864
  for (const fact of allValidFacts) {
2729
2865
  const id = generateId("fact_");
2730
- await this.db.runAsync(
2731
- `INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at)
2732
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2733
- [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "immutable_document", sourceHash, sourceRef, now, now]
2734
- );
2866
+ const wikiFact = {
2867
+ id,
2868
+ entity_id: entityId,
2869
+ title: fact.title,
2870
+ body: fact.body,
2871
+ tags: fact.tags,
2872
+ confidence: fact.confidence,
2873
+ source_type: "immutable_document",
2874
+ source_hash: sourceHash,
2875
+ source_ref: sourceRef,
2876
+ created_at: now,
2877
+ updated_at: now,
2878
+ last_accessed_at: null,
2879
+ access_count: 0,
2880
+ deleted_at: null
2881
+ };
2882
+ await this.entryRepo.upsert(wikiFact);
2735
2883
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
2736
2884
  }
2737
2885
  });