@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.d.mts CHANGED
@@ -357,6 +357,7 @@ declare class WikiMemory {
357
357
  private db;
358
358
  private prefix;
359
359
  private options;
360
+ private entryRepo;
360
361
  private activeMaintenanceJobs;
361
362
  private activeIngestJobs;
362
363
  private statusSubscribers;
package/dist/index.d.ts CHANGED
@@ -357,6 +357,7 @@ declare class WikiMemory {
357
357
  private db;
358
358
  private prefix;
359
359
  private options;
360
+ private entryRepo;
360
361
  private activeMaintenanceJobs;
361
362
  private activeIngestJobs;
362
363
  private statusSubscribers;
package/dist/index.js CHANGED
@@ -145,6 +145,157 @@ var PrunePartialFailureError = class extends Error {
145
145
  }
146
146
  };
147
147
 
148
+ // src/repositories/BaseRepository.ts
149
+ var BaseRepository = class {
150
+ constructor(db, prefix) {
151
+ this.db = db;
152
+ this.prefix = prefix;
153
+ }
154
+ /**
155
+ * Return the DB executor for a given transaction handle.
156
+ * If tx is provided, use it; otherwise fall back to this.db.
157
+ */
158
+ getExecutor(tx) {
159
+ return tx ?? this.db;
160
+ }
161
+ };
162
+
163
+ // src/repositories/EntryRepository.ts
164
+ function mapRowToFact(row) {
165
+ const tags = (() => {
166
+ if (Array.isArray(row.tags)) return row.tags;
167
+ try {
168
+ const p = JSON.parse(row.tags);
169
+ if (Array.isArray(p)) return p;
170
+ } catch {
171
+ }
172
+ return [];
173
+ })();
174
+ return {
175
+ id: row.id,
176
+ entity_id: row.entity_id,
177
+ title: row.title,
178
+ body: row.body,
179
+ tags,
180
+ confidence: row.confidence,
181
+ source_type: row.source_type,
182
+ source_hash: row.source_hash ?? null,
183
+ source_ref: row.source_ref ?? null,
184
+ created_at: Number(row.created_at),
185
+ updated_at: Number(row.updated_at),
186
+ last_accessed_at: row.last_accessed_at === null || row.last_accessed_at === void 0 ? null : Number(row.last_accessed_at),
187
+ deleted_at: row.deleted_at != null ? Number(row.deleted_at) : null,
188
+ access_count: Number(row.access_count ?? 0)
189
+ };
190
+ }
191
+ var EntryRepository = class extends BaseRepository {
192
+ constructor() {
193
+ super(...arguments);
194
+ this.chunkSize = 500;
195
+ }
196
+ /**
197
+ * Fetch facts by IDs, optionally scoped to entity IDs.
198
+ * Returns facts in the order of the input IDs (first match wins).
199
+ */
200
+ async findByIds(ids, scopedEntityIds, tx) {
201
+ const executor = this.getExecutor(tx);
202
+ const rows = [];
203
+ const entityClause = scopedEntityIds && scopedEntityIds.length > 0 ? ` AND entity_id IN (${scopedEntityIds.map(() => "?").join(",")})` : "";
204
+ const entityParams = scopedEntityIds ?? [];
205
+ for (let i = 0; i < ids.length; i += this.chunkSize) {
206
+ const chunk = ids.slice(i, i + this.chunkSize);
207
+ const placeholders = chunk.map(() => "?").join(",");
208
+ const chunkRows = await executor.getAllAsync(
209
+ `SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders})${entityClause} AND deleted_at IS NULL`,
210
+ [...chunk, ...entityParams]
211
+ );
212
+ rows.push(...chunkRows);
213
+ }
214
+ const byId = new Map(rows.map((r) => [r.id, r]));
215
+ return ids.map((id) => byId.get(id)).filter((r) => r !== void 0).map(mapRowToFact);
216
+ }
217
+ /**
218
+ * Upsert a WikiFact. Nullable fields set to null when fact value is null.
219
+ * Returns { changes, lastInsertRowId }.
220
+ */
221
+ async upsert(fact, tx) {
222
+ const executor = this.getExecutor(tx);
223
+ const now = Date.now();
224
+ const tagsJson = JSON.stringify(fact.tags);
225
+ 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" ? (() => {
226
+ const obj = fact.embedding_blob;
227
+ const keys = Object.keys(obj).map(Number).sort((a, b) => a - b);
228
+ const arr = new Uint8Array(keys.length);
229
+ for (let i = 0; i < keys.length; i++) arr[i] = obj[String(keys[i])];
230
+ return arr;
231
+ })() : void 0;
232
+ return executor.runAsync(
233
+ `INSERT INTO ${this.prefix}entries (
234
+ id, entity_id, title, body, tags, confidence, source_type,
235
+ source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count,
236
+ deleted_at, embedding_blob, embedding
237
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
238
+ ON CONFLICT(id) DO UPDATE SET
239
+ entity_id = excluded.entity_id,
240
+ title = excluded.title,
241
+ body = excluded.body,
242
+ tags = excluded.tags,
243
+ confidence = excluded.confidence,
244
+ source_type = excluded.source_type,
245
+ source_hash = excluded.source_hash,
246
+ source_ref = excluded.source_ref,
247
+ updated_at = excluded.updated_at,
248
+ last_accessed_at = excluded.last_accessed_at,
249
+ access_count = excluded.access_count,
250
+ deleted_at = excluded.deleted_at,
251
+ embedding_blob = CASE WHEN excluded.embedding_blob IS NULL THEN embedding_blob ELSE excluded.embedding_blob END,
252
+ embedding = NULL`,
253
+ [
254
+ fact.id,
255
+ fact.entity_id,
256
+ fact.title,
257
+ fact.body,
258
+ tagsJson,
259
+ fact.confidence,
260
+ fact.source_type,
261
+ fact.source_hash,
262
+ fact.source_ref,
263
+ fact.created_at,
264
+ now,
265
+ // updated_at set by repo
266
+ fact.last_accessed_at === null ? null : fact.last_accessed_at,
267
+ fact.access_count,
268
+ fact.deleted_at ?? null,
269
+ embeddingBlob ?? null,
270
+ null
271
+ ]
272
+ );
273
+ }
274
+ /**
275
+ * Soft-delete a single entry by ID scoped to entityId. Sets deleted_at + updated_at.
276
+ */
277
+ async softDelete(entryId, entityId, tx) {
278
+ const executor = this.getExecutor(tx);
279
+ const now = Date.now();
280
+ return executor.runAsync(
281
+ `UPDATE ${this.prefix}entries SET deleted_at = ?, updated_at = ? WHERE id = ? AND entity_id = ? AND deleted_at IS NULL`,
282
+ [now, now, entryId, entityId]
283
+ );
284
+ }
285
+ /**
286
+ * Fetch IDs + entity_ids of soft-deleted rows older than cutoff for a given entity.
287
+ * Used by runPrune().
288
+ */
289
+ async getPrunableMetadata(entityId, cutoff, tx) {
290
+ const executor = this.getExecutor(tx);
291
+ return executor.getAllAsync(
292
+ `SELECT id, entity_id FROM ${this.prefix}entries
293
+ WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
294
+ [entityId, cutoff]
295
+ );
296
+ }
297
+ };
298
+
148
299
  // src/prompts.ts
149
300
  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.
150
301
  Return ONLY a valid JSON object matching this schema:
@@ -467,6 +618,7 @@ var _WikiMemory = class _WikiMemory {
467
618
  this.db = db;
468
619
  this.options = options;
469
620
  this.prefix = options.config?.tablePrefix || "llm_wiki_";
621
+ this.entryRepo = new EntryRepository(db, this.prefix);
470
622
  }
471
623
  normalizeMiniSearchRow(row) {
472
624
  return {
@@ -922,11 +1074,7 @@ After running the migration SQL, restart your application.`
922
1074
  let deletedEvents = 0;
923
1075
  if (retainSoftDeletedFor !== null) {
924
1076
  const cutoff = now - retainSoftDeletedFor * 864e5;
925
- const entriesToDelete = await this.db.getAllAsync(
926
- `SELECT id, entity_id FROM ${this.prefix}entries
927
- WHERE entity_id = ? AND deleted_at IS NOT NULL AND deleted_at <= ?`,
928
- [entityId, cutoff]
929
- );
1077
+ const entriesToDelete = await this.entryRepo.getPrunableMetadata(entityId, cutoff);
930
1078
  const succeeded = [];
931
1079
  let failure = null;
932
1080
  for (const row of entriesToDelete) {
@@ -1482,7 +1630,15 @@ After running the migration SQL, restart your application.`
1482
1630
  const { embedding: _embedding, embedding_blob: _blob, ...rest } = f;
1483
1631
  return {
1484
1632
  ...rest,
1485
- tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
1633
+ tags: (() => {
1634
+ if (Array.isArray(rest.tags)) return rest.tags;
1635
+ try {
1636
+ const p = JSON.parse(rest.tags);
1637
+ return Array.isArray(p) ? p : [];
1638
+ } catch {
1639
+ return [];
1640
+ }
1641
+ })()
1486
1642
  };
1487
1643
  });
1488
1644
  }
@@ -1562,28 +1718,8 @@ After running the migration SQL, restart your application.`
1562
1718
  * Hydrate full facts by ID. Pass scopedEntityIds to restrict to requested namespaces in SQL
1563
1719
  * (defense-in-depth against a rogue VectorRanker returning cross-entity IDs).
1564
1720
  */
1565
- async _hydrateFactsByIds(ids, scopedEntityIds) {
1566
- const fullRows = [];
1567
- const chunkSize = 500;
1568
- const entityClause = scopedEntityIds && scopedEntityIds.length > 0 ? ` AND entity_id IN (${scopedEntityIds.map(() => "?").join(",")})` : "";
1569
- const entityParams = scopedEntityIds && scopedEntityIds.length > 0 ? [...scopedEntityIds] : [];
1570
- for (let i = 0; i < ids.length; i += chunkSize) {
1571
- const idChunk = ids.slice(i, i + chunkSize);
1572
- const placeholders = idChunk.map(() => "?").join(",");
1573
- const chunkRows = await this.db.getAllAsync(
1574
- `SELECT * FROM ${this.prefix}entries WHERE id IN (${placeholders})${entityClause} AND deleted_at IS NULL`,
1575
- [...idChunk, ...entityParams]
1576
- );
1577
- fullRows.push(...chunkRows);
1578
- }
1579
- const byId = new Map(fullRows.map((row) => [row.id, row]));
1580
- return ids.map((id) => byId.get(id)).filter((fact) => fact !== void 0).map((fact) => {
1581
- const { embedding: _embedding, embedding_blob: _blob, ...rest } = fact;
1582
- return {
1583
- ...rest,
1584
- tags: typeof rest.tags === "string" ? JSON.parse(rest.tags) : rest.tags
1585
- };
1586
- });
1721
+ async _hydrateFactsByIds(ids, scopedEntityIds, tx) {
1722
+ return this.entryRepo.findByIds(ids, scopedEntityIds, tx);
1587
1723
  }
1588
1724
  /**
1589
1725
  * Strip potentially sensitive data from ranker errors before exposing to host callbacks.
@@ -2596,7 +2732,7 @@ The following document anchors are provided for contradiction detection only. Do
2596
2732
  const entriesToDelete = await this.db.getAllAsync(q, args);
2597
2733
  deletedEntryIds.push(...entriesToDelete.map((e) => e.id));
2598
2734
  }
2599
- 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;
2735
+ const entryPromise = params.entryId ? this.entryRepo.softDelete(params.entryId, entityId) : null;
2600
2736
  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;
2601
2737
  let refPromise = null;
2602
2738
  if (sourceRef || sourceHash) {
@@ -2733,11 +2869,23 @@ ${chunk}`;
2733
2869
  );
2734
2870
  for (const fact of allValidFacts) {
2735
2871
  const id = generateId("fact_");
2736
- await this.db.runAsync(
2737
- `INSERT INTO ${this.prefix}entries (id, entity_id, title, body, tags, confidence, source_type, source_hash, source_ref, created_at, updated_at)
2738
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2739
- [id, entityId, fact.title, fact.body, JSON.stringify(fact.tags), fact.confidence, "immutable_document", sourceHash, sourceRef, now, now]
2740
- );
2872
+ const wikiFact = {
2873
+ id,
2874
+ entity_id: entityId,
2875
+ title: fact.title,
2876
+ body: fact.body,
2877
+ tags: fact.tags,
2878
+ confidence: fact.confidence,
2879
+ source_type: "immutable_document",
2880
+ source_hash: sourceHash,
2881
+ source_ref: sourceRef,
2882
+ created_at: now,
2883
+ updated_at: now,
2884
+ last_accessed_at: null,
2885
+ access_count: 0,
2886
+ deleted_at: null
2887
+ };
2888
+ await this.entryRepo.upsert(wikiFact);
2741
2889
  insertedFacts.push({ id, entity_id: entityId, title: fact.title, body: fact.body, tags: JSON.stringify(fact.tags) });
2742
2890
  }
2743
2891
  });