@equationalapplications/core-llm-wiki 4.14.0 → 4.15.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
@@ -34,7 +34,8 @@ async function setupDatabase(db, prefix) {
34
34
  access_count INTEGER NOT NULL DEFAULT 0,
35
35
  deleted_at INTEGER,
36
36
  embedding TEXT,
37
- embedding_blob BLOB
37
+ embedding_blob BLOB,
38
+ okf_type TEXT
38
39
  );
39
40
 
40
41
  CREATE INDEX IF NOT EXISTS ${prefix}entries_entity_idx ON ${prefix}entries(entity_id);
@@ -51,11 +52,24 @@ async function setupDatabase(db, prefix) {
51
52
  created_at INTEGER NOT NULL,
52
53
  updated_at INTEGER NOT NULL,
53
54
  resolved_at INTEGER,
54
- deleted_at INTEGER
55
+ deleted_at INTEGER,
56
+ okf_type TEXT
55
57
  );
56
58
 
57
59
  CREATE INDEX IF NOT EXISTS ${prefix}tasks_entity_idx ON ${prefix}tasks(entity_id, status);
58
60
 
61
+ CREATE TABLE IF NOT EXISTS ${prefix}edges (
62
+ id TEXT PRIMARY KEY,
63
+ entity_id TEXT NOT NULL,
64
+ source_id TEXT NOT NULL,
65
+ target_id TEXT NOT NULL,
66
+ edge_type TEXT NOT NULL,
67
+ created_at INTEGER NOT NULL,
68
+ UNIQUE(entity_id, source_id, target_id, edge_type)
69
+ );
70
+
71
+ CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_idx ON ${prefix}edges(entity_id);
72
+
59
73
  CREATE TABLE IF NOT EXISTS ${prefix}events (
60
74
  id TEXT PRIMARY KEY,
61
75
  entity_id TEXT NOT NULL,
@@ -156,6 +170,32 @@ var MIGRATIONS = [
156
170
  ON ${prefix}outbox (entity_id, created_at);
157
171
  `);
158
172
  }
173
+ },
174
+ {
175
+ version: 5,
176
+ description: "Add okf_type to entries/tasks for OKF type fidelity; create edges table for OKF graph import",
177
+ run: async (db, prefix) => {
178
+ for (const table of ["entries", "tasks"]) {
179
+ const cols = await db.getAllAsync(
180
+ `PRAGMA table_info(${prefix}${table})`
181
+ );
182
+ if (!cols.some((c) => c.name === "okf_type")) {
183
+ await db.execAsync(`ALTER TABLE ${prefix}${table} ADD COLUMN okf_type TEXT`);
184
+ }
185
+ }
186
+ await db.execAsync(`
187
+ CREATE TABLE IF NOT EXISTS ${prefix}edges (
188
+ id TEXT PRIMARY KEY,
189
+ entity_id TEXT NOT NULL,
190
+ source_id TEXT NOT NULL,
191
+ target_id TEXT NOT NULL,
192
+ edge_type TEXT NOT NULL,
193
+ created_at INTEGER NOT NULL,
194
+ UNIQUE(entity_id, source_id, target_id, edge_type)
195
+ );
196
+ CREATE INDEX IF NOT EXISTS ${prefix}edges_entity_idx ON ${prefix}edges (entity_id);
197
+ `);
198
+ }
159
199
  }
160
200
  ];
161
201
  for (let i = 1; i < MIGRATIONS.length; i++) {
@@ -207,7 +247,8 @@ function mapRowToFact(row) {
207
247
  updated_at: Number(row.updated_at),
208
248
  last_accessed_at: row.last_accessed_at === null || row.last_accessed_at === void 0 ? null : Number(row.last_accessed_at),
209
249
  deleted_at: row.deleted_at != null ? Number(row.deleted_at) : null,
210
- access_count: Number(row.access_count ?? 0)
250
+ access_count: Number(row.access_count ?? 0),
251
+ okf_type: row.okf_type ?? null
211
252
  };
212
253
  }
213
254
  function normalizeEmbeddingBlobValue(blob) {
@@ -380,8 +421,8 @@ var EntryRepository = class extends BaseRepository {
380
421
  `INSERT INTO ${this.prefix}entries (
381
422
  id, entity_id, title, body, tags, confidence, source_type,
382
423
  source_hash, source_ref, created_at, updated_at, last_accessed_at, access_count,
383
- deleted_at, embedding_blob, embedding
384
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
424
+ deleted_at, embedding_blob, embedding, okf_type
425
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
385
426
  ON CONFLICT(id) DO UPDATE SET
386
427
  entity_id = excluded.entity_id,
387
428
  title = excluded.title,
@@ -397,7 +438,8 @@ var EntryRepository = class extends BaseRepository {
397
438
  access_count = excluded.access_count,
398
439
  deleted_at = excluded.deleted_at,
399
440
  embedding_blob = excluded.embedding_blob,
400
- embedding = NULL`,
441
+ embedding = NULL,
442
+ okf_type = excluded.okf_type`,
401
443
  [
402
444
  fact.id,
403
445
  fact.entity_id,
@@ -414,7 +456,8 @@ var EntryRepository = class extends BaseRepository {
414
456
  fact.access_count,
415
457
  fact.deleted_at ?? null,
416
458
  embeddingBlob ?? null,
417
- null
459
+ null,
460
+ fact.okf_type ?? null
418
461
  ]
419
462
  );
420
463
  return result;
@@ -926,7 +969,9 @@ function generateId(prefix = "") {
926
969
  crypto.getRandomValues(bytes);
927
970
  return prefix + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("").substring(0, 24);
928
971
  }
929
- return prefix + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
972
+ throw new Error(
973
+ "generateId: no cryptographically secure random source available (crypto.randomUUID and crypto.getRandomValues are both missing)."
974
+ );
930
975
  }
931
976
 
932
977
  // src/repositories/OutboxRepository.ts
@@ -992,7 +1037,8 @@ function mapRowToTask(row) {
992
1037
  created_at: Number(row.created_at),
993
1038
  updated_at: Number(row.updated_at),
994
1039
  resolved_at: row.resolved_at != null ? Number(row.resolved_at) : null,
995
- deleted_at: row.deleted_at != null ? Number(row.deleted_at) : null
1040
+ deleted_at: row.deleted_at != null ? Number(row.deleted_at) : null,
1041
+ okf_type: row.okf_type ?? null
996
1042
  };
997
1043
  }
998
1044
  var TaskRepository = class extends BaseRepository {
@@ -1094,8 +1140,8 @@ var TaskRepository = class extends BaseRepository {
1094
1140
  await executor.runAsync(
1095
1141
  `INSERT INTO ${this.prefix}tasks (
1096
1142
  id, entity_id, description, status, priority,
1097
- created_at, updated_at, resolved_at, deleted_at
1098
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1143
+ created_at, updated_at, resolved_at, deleted_at, okf_type
1144
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1099
1145
  ON CONFLICT(id) DO UPDATE SET
1100
1146
  entity_id = excluded.entity_id,
1101
1147
  description = excluded.description,
@@ -1103,7 +1149,8 @@ var TaskRepository = class extends BaseRepository {
1103
1149
  priority = excluded.priority,
1104
1150
  updated_at = excluded.updated_at,
1105
1151
  resolved_at = excluded.resolved_at,
1106
- deleted_at = excluded.deleted_at`,
1152
+ deleted_at = excluded.deleted_at,
1153
+ okf_type = excluded.okf_type`,
1107
1154
  [
1108
1155
  task.id,
1109
1156
  task.entity_id,
@@ -1113,7 +1160,8 @@ var TaskRepository = class extends BaseRepository {
1113
1160
  task.created_at,
1114
1161
  now,
1115
1162
  task.resolved_at ?? null,
1116
- task.deleted_at ?? null
1163
+ task.deleted_at ?? null,
1164
+ task.okf_type ?? null
1117
1165
  ]
1118
1166
  );
1119
1167
  }
@@ -1335,6 +1383,53 @@ var EventRepository = class extends BaseRepository {
1335
1383
  }
1336
1384
  };
1337
1385
 
1386
+ // src/repositories/EdgeRepository.ts
1387
+ var EdgeRepository = class extends BaseRepository {
1388
+ /**
1389
+ * Insert an edge, silently skipping on primary-key or uniqueness conflicts.
1390
+ * Throws when the insert was skipped due to an id collision with a different edge tuple.
1391
+ */
1392
+ async addIgnoreDuplicate(edge, tx) {
1393
+ const executor = this.getExecutor(tx);
1394
+ const result = await executor.runAsync(
1395
+ `INSERT OR IGNORE INTO ${this.prefix}edges (id, entity_id, source_id, target_id, edge_type, created_at)
1396
+ VALUES (?, ?, ?, ?, ?, ?)`,
1397
+ [edge.id, edge.entity_id, edge.source_id, edge.target_id, edge.edge_type, edge.created_at]
1398
+ );
1399
+ if (result.changes > 0) return;
1400
+ const existing = await executor.getFirstAsync(
1401
+ `SELECT entity_id, source_id, target_id, edge_type FROM ${this.prefix}edges WHERE id = ?`,
1402
+ [edge.id]
1403
+ );
1404
+ if (!existing) return;
1405
+ if (String(existing.entity_id) !== edge.entity_id || String(existing.source_id) !== edge.source_id || String(existing.target_id) !== edge.target_id || String(existing.edge_type) !== edge.edge_type) {
1406
+ throw new Error(
1407
+ `Edge id collision: ${JSON.stringify(edge.id)} already exists with a different (entity_id, source_id, target_id, edge_type) tuple`
1408
+ );
1409
+ }
1410
+ }
1411
+ async getByEntityId(entityId, tx) {
1412
+ const executor = this.getExecutor(tx);
1413
+ const rows = await executor.getAllAsync(
1414
+ `SELECT * FROM ${this.prefix}edges WHERE entity_id = ? ORDER BY created_at ASC`,
1415
+ [entityId]
1416
+ );
1417
+ return rows.map((row) => ({
1418
+ id: String(row.id),
1419
+ entity_id: String(row.entity_id),
1420
+ source_id: String(row.source_id),
1421
+ target_id: String(row.target_id),
1422
+ edge_type: String(row.edge_type),
1423
+ created_at: Number(row.created_at)
1424
+ }));
1425
+ }
1426
+ /** Hard delete — edges have no soft-delete concept, only presence/absence. `tx` is REQUIRED. */
1427
+ async bulkDeleteByEntityId(entityId, tx) {
1428
+ const executor = this.getExecutor(tx);
1429
+ await executor.runAsync(`DELETE FROM ${this.prefix}edges WHERE entity_id = ?`, [entityId]);
1430
+ }
1431
+ };
1432
+
1338
1433
  // src/repositories/MetadataRepository.ts
1339
1434
  var MetadataRepository = class extends BaseRepository {
1340
1435
  // CHECKPOINTS TABLE METHODS
@@ -1711,6 +1806,20 @@ var JobManager = class {
1711
1806
  this.activeMaintenanceJobs = /* @__PURE__ */ new Set();
1712
1807
  this.activeIngestJobs = /* @__PURE__ */ new Map();
1713
1808
  this.statusSubscribers = /* @__PURE__ */ new Map();
1809
+ /**
1810
+ * Lookup table for acquireLock/releaseLock's dynamic-dispatch branch.
1811
+ * Excludes 'ingest' | 'global_reembed' | 'global_import', which those
1812
+ * methods already handle via explicit if/else branches before reaching
1813
+ * this table.
1814
+ */
1815
+ this.lockKeyFns = {
1816
+ prune: (id) => this._pruneKey(id),
1817
+ librarian: (id) => this._librarianKey(id),
1818
+ heal: (id) => this._healKey(id),
1819
+ reembed: (id) => this._reembedKey(id),
1820
+ import: (id) => this._importKey(id),
1821
+ forget: (id) => this._forgetKey(id)
1822
+ };
1714
1823
  }
1715
1824
  _pruneKey(entityId) {
1716
1825
  return `${this.prefix}:${entityId}:prune`;
@@ -1860,9 +1969,7 @@ var JobManager = class {
1860
1969
  } else if (operation === "global_import") {
1861
1970
  this.activeMaintenanceJobs.add(this._globalImportKey());
1862
1971
  } else {
1863
- const keyFnName = `_${operation}Key`;
1864
- const keyFn = this[keyFnName];
1865
- this.activeMaintenanceJobs.add(keyFn.call(this, entityId));
1972
+ this.activeMaintenanceJobs.add(this.lockKeyFns[operation](entityId));
1866
1973
  }
1867
1974
  this._notifyStatusSubscribers(entityId);
1868
1975
  }
@@ -1874,9 +1981,7 @@ var JobManager = class {
1874
1981
  } else if (operation === "global_import") {
1875
1982
  this.activeMaintenanceJobs.delete(this._globalImportKey());
1876
1983
  } else {
1877
- const keyFnName = `_${operation}Key`;
1878
- const keyFn = this[keyFnName];
1879
- this.activeMaintenanceJobs.delete(keyFn.call(this, entityId));
1984
+ this.activeMaintenanceJobs.delete(this.lockKeyFns[operation](entityId));
1880
1985
  }
1881
1986
  this._notifyStatusSubscribers(entityId);
1882
1987
  }
@@ -2826,12 +2931,16 @@ var MaintenanceService = class {
2826
2931
  };
2827
2932
 
2828
2933
  // src/services/ImportExportService.ts
2934
+ var MAX_EMBEDDING_BLOB_BYTES = 32 * 1024;
2935
+ var IMPORT_TITLE_MAX = 500;
2936
+ var IMPORT_BODY_MAX = 8e3;
2829
2937
  var ImportExportService = class {
2830
- constructor(db, entryRepo, taskRepo, eventRepo, metadataRepo, searchService, jobManager, embeddingService) {
2938
+ constructor(db, entryRepo, taskRepo, eventRepo, edgeRepo, metadataRepo, searchService, jobManager, embeddingService) {
2831
2939
  this.db = db;
2832
2940
  this.entryRepo = entryRepo;
2833
2941
  this.taskRepo = taskRepo;
2834
2942
  this.eventRepo = eventRepo;
2943
+ this.edgeRepo = edgeRepo;
2835
2944
  this.metadataRepo = metadataRepo;
2836
2945
  this.searchService = searchService;
2837
2946
  this.jobManager = jobManager;
@@ -2876,10 +2985,11 @@ var ImportExportService = class {
2876
2985
  }
2877
2986
  }
2878
2987
  async getFullBundle(entityId, opts) {
2879
- const [factsRaw, tasks, events] = await Promise.all([
2988
+ const [factsRaw, tasks, events, edges] = await Promise.all([
2880
2989
  opts?.includeBlobs ? this.entryRepo.findAllByEntityIdWithBlobs(entityId) : this.entryRepo.findAllByEntityId(entityId),
2881
2990
  this.taskRepo.findAllByEntityId(entityId),
2882
- this.eventRepo.getByEntityId(entityId, opts?.maxEvents)
2991
+ this.eventRepo.getByEntityId(entityId, opts?.maxEvents),
2992
+ this.edgeRepo.getByEntityId(entityId)
2883
2993
  ]);
2884
2994
  const facts = factsRaw.map((f) => {
2885
2995
  const {
@@ -2898,7 +3008,7 @@ var ImportExportService = class {
2898
3008
  tags: typeof factBase.tags === "string" ? JSON.parse(factBase.tags) : factBase.tags
2899
3009
  };
2900
3010
  });
2901
- return { facts, tasks, events };
3011
+ return { facts, tasks, events, edges };
2902
3012
  }
2903
3013
  /** Single-entity import transaction + post-processing; package-internal hook for tests. */
2904
3014
  async doImportEntity(entityId, bundle, merge) {
@@ -2907,6 +3017,7 @@ var ImportExportService = class {
2907
3017
  const factsWithPreservedBlob = /* @__PURE__ */ new Map();
2908
3018
  const preservedBlobDims = /* @__PURE__ */ new Set();
2909
3019
  const softDeletedFactIds = [];
3020
+ const clippedTextByFactId = /* @__PURE__ */ new Map();
2910
3021
  await this.db.withTransactionAsync(async (tx) => {
2911
3022
  if (!merge) {
2912
3023
  const deletedLiveFactIds = await this.entryRepo.findIdsBySource(
@@ -2919,6 +3030,7 @@ var ImportExportService = class {
2919
3030
  softDeletedFactIds.push(...deletedLiveFactIds);
2920
3031
  await this.entryRepo.bulkSoftDeleteByEntityId(entityId, tx);
2921
3032
  await this.taskRepo.bulkSoftDeleteByEntityId(entityId, tx);
3033
+ await this.edgeRepo.bulkDeleteByEntityId(entityId, tx);
2922
3034
  await this.metadataRepo.deleteCheckpoint(entityId, tx);
2923
3035
  }
2924
3036
  const factIds = bundle.facts.map((fact) => fact.id);
@@ -2943,21 +3055,32 @@ var ImportExportService = class {
2943
3055
  const rawBlobRaw = fact.embedding_blob;
2944
3056
  let rawBlob = null;
2945
3057
  if (rawBlobRaw instanceof Uint8Array) {
2946
- rawBlob = rawBlobRaw;
3058
+ if (rawBlobRaw.byteLength <= MAX_EMBEDDING_BLOB_BYTES) {
3059
+ rawBlob = rawBlobRaw;
3060
+ }
2947
3061
  } else if (rawBlobRaw !== null && rawBlobRaw !== void 0 && typeof rawBlobRaw === "object") {
2948
3062
  const obj = rawBlobRaw;
2949
3063
  if (obj["type"] === "Buffer" && Array.isArray(obj["data"])) {
2950
- rawBlob = new Uint8Array(obj["data"]);
3064
+ const data = obj["data"];
3065
+ if (data.length <= MAX_EMBEDDING_BLOB_BYTES) {
3066
+ rawBlob = new Uint8Array(data);
3067
+ }
2951
3068
  } else if (!Array.isArray(rawBlobRaw)) {
2952
3069
  const entries = Object.keys(obj);
2953
3070
  if (entries.length > 0 && entries.every((k) => /^\d+$/.test(k))) {
2954
3071
  const len = entries.length;
2955
- rawBlob = new Uint8Array(len);
2956
- for (let i = 0; i < len; i++)
2957
- rawBlob[i] = obj[String(i)] ?? 0;
3072
+ if (len <= MAX_EMBEDDING_BLOB_BYTES) {
3073
+ rawBlob = new Uint8Array(len);
3074
+ for (let i = 0; i < len; i++) {
3075
+ rawBlob[i] = obj[String(i)] ?? 0;
3076
+ }
3077
+ }
2958
3078
  }
2959
3079
  }
2960
3080
  }
3081
+ if (rawBlob !== null && rawBlob.byteLength > MAX_EMBEDDING_BLOB_BYTES) {
3082
+ rawBlob = null;
3083
+ }
2961
3084
  let blobData = null;
2962
3085
  if (rawBlob !== null && rawBlob.byteLength > 0 && rawBlob.byteLength % 4 === 0) {
2963
3086
  const copy = new ArrayBuffer(rawBlob.byteLength);
@@ -2987,11 +3110,14 @@ var ImportExportService = class {
2987
3110
  }
2988
3111
  if (merge && safeUpdatedAt <= existing.updated_at) continue;
2989
3112
  }
3113
+ const safeTitle = clip(String(fact.title ?? ""), IMPORT_TITLE_MAX);
3114
+ const safeBody = clip(String(fact.body ?? ""), IMPORT_BODY_MAX);
3115
+ clippedTextByFactId.set(fact.id, { title: safeTitle, body: safeBody });
2990
3116
  const factObj = {
2991
3117
  id: fact.id,
2992
3118
  entity_id: entityId,
2993
- title: fact.title,
2994
- body: fact.body,
3119
+ title: safeTitle,
3120
+ body: safeBody,
2995
3121
  tags: Array.isArray(fact.tags) ? fact.tags : [],
2996
3122
  confidence: fact.confidence,
2997
3123
  source_type: sourceType,
@@ -3002,7 +3128,8 @@ var ImportExportService = class {
3002
3128
  last_accessed_at: fact.last_accessed_at,
3003
3129
  access_count: fact.access_count,
3004
3130
  deleted_at: fact.deleted_at,
3005
- embedding_blob: blobData ?? void 0
3131
+ embedding_blob: blobData ?? void 0,
3132
+ okf_type: fact.okf_type ?? null
3006
3133
  };
3007
3134
  await this.entryRepo.upsertForImport(factObj, tx);
3008
3135
  if (blobData != null) {
@@ -3051,7 +3178,8 @@ var ImportExportService = class {
3051
3178
  created_at: task.created_at,
3052
3179
  updated_at: safeUpdatedAt,
3053
3180
  resolved_at: task.resolved_at,
3054
- deleted_at: task.deleted_at
3181
+ deleted_at: task.deleted_at,
3182
+ okf_type: task.okf_type ?? null
3055
3183
  },
3056
3184
  tx,
3057
3185
  safeUpdatedAt
@@ -3075,15 +3203,29 @@ var ImportExportService = class {
3075
3203
  tx
3076
3204
  );
3077
3205
  }
3206
+ for (const edge of bundle.edges ?? []) {
3207
+ await this.edgeRepo.addIgnoreDuplicate(
3208
+ {
3209
+ id: edge.id,
3210
+ entity_id: entityId,
3211
+ source_id: edge.source_id,
3212
+ target_id: edge.target_id,
3213
+ edge_type: edge.edge_type,
3214
+ created_at: edge.created_at
3215
+ },
3216
+ tx
3217
+ );
3218
+ }
3078
3219
  });
3079
3220
  await this.searchService.sync(entityId);
3080
3221
  for (const fact of bundle.facts) {
3081
3222
  if (!fact.deleted_at && upsertedFactIds.has(fact.id) && !factsWithPreservedBlob.has(fact.id)) {
3223
+ const clipped = clippedTextByFactId.get(fact.id);
3082
3224
  const embedded = await this.embeddingService.embedFact({
3083
3225
  id: fact.id,
3084
3226
  entity_id: entityId,
3085
- title: fact.title,
3086
- body: fact.body,
3227
+ title: clipped?.title ?? fact.title,
3228
+ body: clipped?.body ?? fact.body,
3087
3229
  tags: Array.isArray(fact.tags) || typeof fact.tags === "string" ? fact.tags : []
3088
3230
  });
3089
3231
  if (!embedded) {
@@ -3183,7 +3325,7 @@ var ImportExportService = class {
3183
3325
  }
3184
3326
  _warnCrossEntityCollision(type, id, existingEntityId, targetEntityId) {
3185
3327
  console.warn(
3186
- `[WikiMemory] importDump: ${type} id "${id}" already belongs to entity "${existingEntityId}"; skipping for entity "${targetEntityId}"`
3328
+ `[WikiMemory] importDump: ${type} id ${JSON.stringify(id)} already belongs to entity ${JSON.stringify(existingEntityId)}; skipping for entity ${JSON.stringify(targetEntityId)}`
3187
3329
  );
3188
3330
  }
3189
3331
  _normalizeImportedSourceType(raw, ctx) {
@@ -3262,7 +3404,7 @@ var EmbeddingService = class {
3262
3404
  tagsStr = fact.tags;
3263
3405
  }
3264
3406
  }
3265
- const text = `${fact.title} ${fact.body} ${tagsStr}`.trim();
3407
+ const text = clip(`${fact.title} ${fact.body} ${tagsStr}`.trim(), 16e3);
3266
3408
  try {
3267
3409
  const vector = await embedFn(text);
3268
3410
  if (vector.length === 0 || !vector.every((v) => typeof v === "number" && isFinite(v))) {
@@ -3390,7 +3532,7 @@ var RetrievalService = class {
3390
3532
  const sanitizedTierWeights = shouldExposeReadMetadata(entityId) ? sanitizeTierWeights(entityIds, options?.tierWeights) : void 0;
3391
3533
  const exposeMetadata = shouldExposeReadMetadata(entityId);
3392
3534
  if (entityIds.length === 0) {
3393
- const empty = { facts: [], tasks: [], events: [] };
3535
+ const empty = { facts: [], tasks: [], events: [], edges: [] };
3394
3536
  if (exposeMetadata) {
3395
3537
  empty.metadata = { query, entityIds: [] };
3396
3538
  if (sanitizedTierWeights && Object.keys(sanitizedTierWeights).length > 0) empty.metadata.tierWeights = sanitizedTierWeights;
@@ -3780,7 +3922,7 @@ var RetrievalService = class {
3780
3922
  if (exposeMetadata && trimmedQuery && scoreByFactId) {
3781
3923
  factScores = Object.fromEntries(facts.map((fact) => [fact.id, scoreByFactId.get(fact.id) ?? 0]));
3782
3924
  }
3783
- const bundle = { facts, tasks, events: events.reverse() };
3925
+ const bundle = { facts, tasks, events: events.reverse(), edges: [] };
3784
3926
  if (exposeMetadata) {
3785
3927
  bundle.metadata = { query, entityIds };
3786
3928
  if (sanitizedTierWeights && Object.keys(sanitizedTierWeights).length > 0) bundle.metadata.tierWeights = sanitizedTierWeights;
@@ -3876,15 +4018,38 @@ var RetrievalService = class {
3876
4018
 
3877
4019
  // src/services/WriteService.ts
3878
4020
  var WriteService = class {
3879
- constructor(db, options, eventRepo, metadataRepo, jobManager, maintenanceService) {
4021
+ constructor(db, options, entryRepo, eventRepo, metadataRepo, jobManager, maintenanceService) {
3880
4022
  this.db = db;
3881
4023
  this.options = options;
4024
+ this.entryRepo = entryRepo;
3882
4025
  this.eventRepo = eventRepo;
3883
4026
  this.metadataRepo = metadataRepo;
3884
4027
  this.jobManager = jobManager;
3885
4028
  this.maintenanceService = maintenanceService;
3886
4029
  }
3887
4030
  async write(entityId, event) {
4031
+ if (typeof entityId !== "string" || entityId.length === 0 || entityId.length > 200 || entityId.includes("\0")) {
4032
+ throw new TypeError(
4033
+ `Invalid entityId: must be a non-empty string at most 200 chars with no null bytes; got ${JSON.stringify(entityId)}.`
4034
+ );
4035
+ }
4036
+ if (event === null || typeof event !== "object" || Array.isArray(event)) {
4037
+ throw new TypeError("Invalid event: must be a non-null object.");
4038
+ }
4039
+ if (typeof event.summary !== "string") {
4040
+ throw new TypeError("Invalid event.summary: must be a string.");
4041
+ }
4042
+ const summary = clip(event.summary, 4e3);
4043
+ let relatedEntryId = null;
4044
+ const rawRelatedEntryId = event.related_entry_id;
4045
+ if (rawRelatedEntryId != null && rawRelatedEntryId !== "") {
4046
+ if (typeof rawRelatedEntryId !== "string" || rawRelatedEntryId.length > 200 || rawRelatedEntryId.includes("\0")) {
4047
+ relatedEntryId = null;
4048
+ } else {
4049
+ const existing = await this.entryRepo.findByIds([rawRelatedEntryId], [entityId]);
4050
+ relatedEntryId = existing.length > 0 ? rawRelatedEntryId : null;
4051
+ }
4052
+ }
3888
4053
  const id = generateId("evt_");
3889
4054
  const now = Date.now();
3890
4055
  let eventType = event.event_type;
@@ -3895,8 +4060,8 @@ var WriteService = class {
3895
4060
  id,
3896
4061
  entity_id: entityId,
3897
4062
  event_type: eventType,
3898
- summary: event.summary,
3899
- related_entry_id: event.related_entry_id || null,
4063
+ summary,
4064
+ related_entry_id: relatedEntryId,
3900
4065
  created_at: now
3901
4066
  };
3902
4067
  let shouldRunLibrarian = false;
@@ -3957,6 +4122,7 @@ var WriteService = class {
3957
4122
  };
3958
4123
 
3959
4124
  // src/WikiMemory.ts
4125
+ var TABLE_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,30}_$/;
3960
4126
  var _testAccessNonTestEnvWarned;
3961
4127
  var WikiMemory = class {
3962
4128
  constructor(db, options) {
@@ -3964,11 +4130,17 @@ var WikiMemory = class {
3964
4130
  __privateAdd(this, _testAccessNonTestEnvWarned, false);
3965
4131
  this.db = db;
3966
4132
  this.options = options;
3967
- this.prefix = options.config?.tablePrefix || "llm_wiki_";
4133
+ this.prefix = options.config?.tablePrefix ?? "llm_wiki_";
4134
+ if (!TABLE_PREFIX_PATTERN.test(this.prefix)) {
4135
+ throw new Error(
4136
+ `Invalid tablePrefix: ${JSON.stringify(this.prefix)}. Must match ${TABLE_PREFIX_PATTERN} (letter, then alphanumeric/underscore, ending in "_", max 32 chars total).`
4137
+ );
4138
+ }
3968
4139
  this.outboxRepo = new OutboxRepository(db, this.prefix, !!options.config?.enableOutbox);
3969
4140
  this.entryRepo = new EntryRepository(db, this.prefix, this.outboxRepo);
3970
4141
  this.taskRepo = new TaskRepository(db, this.prefix, this.outboxRepo);
3971
4142
  this.eventRepo = new EventRepository(db, this.prefix);
4143
+ this.edgeRepo = new EdgeRepository(db, this.prefix);
3972
4144
  this.metadataRepo = new MetadataRepository(db, this.prefix);
3973
4145
  this.embeddingService = new EmbeddingService(this.db, this.options, this.entryRepo, this.metadataRepo);
3974
4146
  this.searchService = new SearchService(this.entryRepo);
@@ -4002,6 +4174,7 @@ var WikiMemory = class {
4002
4174
  this.entryRepo,
4003
4175
  this.taskRepo,
4004
4176
  this.eventRepo,
4177
+ this.edgeRepo,
4005
4178
  this.metadataRepo,
4006
4179
  this.searchService,
4007
4180
  this.jobManager,
@@ -4018,6 +4191,7 @@ var WikiMemory = class {
4018
4191
  this.writeService = new WriteService(
4019
4192
  this.db,
4020
4193
  this.options,
4194
+ this.entryRepo,
4021
4195
  this.eventRepo,
4022
4196
  this.metadataRepo,
4023
4197
  this.jobManager,
@@ -4096,7 +4270,7 @@ var WikiMemory = class {
4096
4270
  async hasChanged(entityId, sourceRef, sourceHash) {
4097
4271
  const normalizedRef = normalizeSourceRef(sourceRef);
4098
4272
  if (!normalizedRef) {
4099
- throw new Error(`Invalid sourceRef: "${sourceRef}"`);
4273
+ throw new Error(`Invalid sourceRef: ${JSON.stringify(sourceRef)}`);
4100
4274
  }
4101
4275
  const normalizedHash = normalizeSourceHash(sourceHash);
4102
4276
  if (!normalizedHash) {
@@ -4461,7 +4635,7 @@ function formatMemoryDump(dump) {
4461
4635
  }
4462
4636
  function factFrontmatter(f) {
4463
4637
  return {
4464
- type: "fact",
4638
+ type: f.okf_type ?? "fact",
4465
4639
  title: f.title,
4466
4640
  tags: f.tags,
4467
4641
  timestamp: new Date(f.updated_at).toISOString(),
@@ -4479,7 +4653,7 @@ function factFrontmatter(f) {
4479
4653
  }
4480
4654
  function taskFrontmatter(t) {
4481
4655
  return {
4482
- type: "task",
4656
+ type: t.okf_type ?? "task",
4483
4657
  title: t.description,
4484
4658
  timestamp: new Date(t.updated_at).toISOString(),
4485
4659
  id: t.id,
@@ -4554,6 +4728,227 @@ function formatOkfBundle(dump) {
4554
4728
  });
4555
4729
  return { files };
4556
4730
  }
4731
+ var CONFIDENCE_VALUES = /* @__PURE__ */ new Set(["certain", "inferred", "tentative"]);
4732
+ var SOURCE_TYPES = /* @__PURE__ */ new Set([
4733
+ "user_stated",
4734
+ "librarian_inferred",
4735
+ "user_confirmed",
4736
+ "immutable_document"
4737
+ ]);
4738
+ var TASK_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "done", "abandoned"]);
4739
+ var EVENT_TYPES = /* @__PURE__ */ new Set(["observation", "decision", "action", "outcome"]);
4740
+ function basenameMd(filePath) {
4741
+ const name = filePath.slice(filePath.lastIndexOf("/") + 1);
4742
+ return name.endsWith(".md") ? name.slice(0, -3) : name;
4743
+ }
4744
+ function isConceptFile(filePath) {
4745
+ if (!filePath.endsWith(".md")) return false;
4746
+ if (filePath.endsWith("/index.md") || filePath === "index.md") return false;
4747
+ if (filePath.endsWith("/log.md") || filePath === "log.md") return false;
4748
+ return true;
4749
+ }
4750
+ function isStructuralPath(filePath) {
4751
+ return filePath.endsWith("/index.md") || filePath === "index.md" || filePath.endsWith("/log.md") || filePath === "log.md";
4752
+ }
4753
+ function posixDirname(filePath) {
4754
+ const idx = filePath.lastIndexOf("/");
4755
+ return idx === -1 ? "" : filePath.slice(0, idx);
4756
+ }
4757
+ function resolveRelativePath(fromFile, linkPath) {
4758
+ const baseDir = posixDirname(fromFile);
4759
+ const segments = [...baseDir ? baseDir.split("/") : [], ...linkPath.split("/")];
4760
+ const resolved = [];
4761
+ for (const seg of segments) {
4762
+ if (seg === "" || seg === ".") continue;
4763
+ if (seg === "..") {
4764
+ resolved.pop();
4765
+ continue;
4766
+ }
4767
+ resolved.push(seg);
4768
+ }
4769
+ return resolved.join("/");
4770
+ }
4771
+ function addPathAliases(map, filePath, resolvedId) {
4772
+ map.set(filePath, resolvedId);
4773
+ const withoutDot = filePath.replace(/^\.\//, "");
4774
+ if (withoutDot !== filePath) map.set(withoutDot, resolvedId);
4775
+ const entityRelative = filePath.replace(/^entities\/[^/]+\//, "");
4776
+ if (entityRelative !== filePath) {
4777
+ map.set(entityRelative, resolvedId);
4778
+ map.set(`./${entityRelative}`, resolvedId);
4779
+ }
4780
+ }
4781
+ function lookupResolvedId(map, path) {
4782
+ const normalized = path.replace(/^\.\//, "");
4783
+ return map.get(path) ?? map.get(normalized) ?? map.get(`./${normalized}`);
4784
+ }
4785
+ function stripLinkSuffix(linkPath) {
4786
+ const hashIdx = linkPath.indexOf("#");
4787
+ const queryIdx = linkPath.indexOf("?");
4788
+ if (hashIdx === -1 && queryIdx === -1) return linkPath;
4789
+ const cut = hashIdx === -1 ? queryIdx : queryIdx === -1 ? hashIdx : Math.min(hashIdx, queryIdx);
4790
+ return linkPath.slice(0, cut);
4791
+ }
4792
+ function resolveRoute(filePath, frontmatterType, options) {
4793
+ if (options?.typeMapping && Object.prototype.hasOwnProperty.call(options.typeMapping, frontmatterType)) {
4794
+ return options.typeMapping[frontmatterType];
4795
+ }
4796
+ if (filePath.includes("/facts/")) return "fact";
4797
+ if (filePath.includes("/tasks/")) return "task";
4798
+ return options?.defaultSchema ?? "fact";
4799
+ }
4800
+ function parseFrontmatterTimestamp(value, fallback) {
4801
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4802
+ if (typeof value === "string") {
4803
+ const parsed = Date.parse(value);
4804
+ if (Number.isFinite(parsed)) return parsed;
4805
+ }
4806
+ return fallback;
4807
+ }
4808
+ function unescapeLogSummary(summary) {
4809
+ return summary.replace(/\\\]/g, "]").replace(/\\\[/g, "[").replace(/\\\\/g, "\\");
4810
+ }
4811
+ var LOG_LINE_PATTERN = /^\(([^)]+)\)\s*(?:\[((?:\\.|[^\]])*)\]\(([^)]+)\)|(.+))$/;
4812
+ function parseLogEntryText(text) {
4813
+ const match = LOG_LINE_PATTERN.exec(text.trim());
4814
+ if (!match) return null;
4815
+ const [, rawType, linkedSummary, linkPath, plainSummary] = match;
4816
+ const event_type = EVENT_TYPES.has(rawType) ? rawType : "observation";
4817
+ if (linkPath) {
4818
+ return { event_type, summary: unescapeLogSummary(linkedSummary), linkPath };
4819
+ }
4820
+ return { event_type, summary: (plainSummary ?? "").trim() };
4821
+ }
4822
+ function frontmatterToFact(entityId, id, frontmatter, body, now) {
4823
+ const created_at = parseFrontmatterTimestamp(frontmatter.created_at, now);
4824
+ const updated_at = parseFrontmatterTimestamp(
4825
+ frontmatter.timestamp,
4826
+ parseFrontmatterTimestamp(frontmatter.updated_at, now)
4827
+ );
4828
+ return {
4829
+ id,
4830
+ entity_id: entityId,
4831
+ title: typeof frontmatter.title === "string" ? frontmatter.title : "",
4832
+ body,
4833
+ tags: Array.isArray(frontmatter.tags) ? frontmatter.tags.filter((t) => typeof t === "string") : [],
4834
+ confidence: CONFIDENCE_VALUES.has(String(frontmatter.confidence)) ? frontmatter.confidence : "tentative",
4835
+ source_type: SOURCE_TYPES.has(String(frontmatter.source_type)) ? frontmatter.source_type : "user_stated",
4836
+ source_hash: typeof frontmatter.source_hash === "string" ? frontmatter.source_hash : null,
4837
+ source_ref: typeof frontmatter.resource === "string" ? frontmatter.resource : null,
4838
+ created_at,
4839
+ updated_at,
4840
+ last_accessed_at: frontmatter.last_accessed_at != null ? parseFrontmatterTimestamp(frontmatter.last_accessed_at, now) : null,
4841
+ access_count: typeof frontmatter.access_count === "number" ? frontmatter.access_count : 0,
4842
+ deleted_at: frontmatter.deleted_at != null ? parseFrontmatterTimestamp(frontmatter.deleted_at, 0) : null,
4843
+ okf_type: frontmatter.type
4844
+ };
4845
+ }
4846
+ function frontmatterToTask(entityId, id, frontmatter, now) {
4847
+ const created_at = parseFrontmatterTimestamp(frontmatter.created_at, now);
4848
+ const updated_at = parseFrontmatterTimestamp(
4849
+ frontmatter.timestamp,
4850
+ parseFrontmatterTimestamp(frontmatter.updated_at, now)
4851
+ );
4852
+ return {
4853
+ id,
4854
+ entity_id: entityId,
4855
+ description: typeof frontmatter.title === "string" ? frontmatter.title : "",
4856
+ status: TASK_STATUSES.has(String(frontmatter.status)) ? frontmatter.status : "pending",
4857
+ priority: typeof frontmatter.priority === "number" ? frontmatter.priority : 0,
4858
+ created_at,
4859
+ updated_at,
4860
+ resolved_at: frontmatter.resolved_at != null ? parseFrontmatterTimestamp(frontmatter.resolved_at, now) : null,
4861
+ deleted_at: frontmatter.deleted_at != null ? parseFrontmatterTimestamp(frontmatter.deleted_at, 0) : null,
4862
+ okf_type: frontmatter.type
4863
+ };
4864
+ }
4865
+ function findLogMdPath(files) {
4866
+ return files.find((f) => f.path.endsWith("/log.md") || f.path === "log.md")?.path;
4867
+ }
4868
+ function parseOkfBundle(entityId, files, options) {
4869
+ const now = Date.now();
4870
+ const pathToResolvedId = /* @__PURE__ */ new Map();
4871
+ for (const file of files) {
4872
+ if (!isConceptFile(file.path)) continue;
4873
+ const { frontmatter } = coreOkf.parseConcept(file.content);
4874
+ const route = resolveRoute(file.path, frontmatter.type ?? "", options);
4875
+ if (route === "ignore") continue;
4876
+ const resolvedId = typeof frontmatter.id === "string" && frontmatter.id ? frontmatter.id : basenameMd(file.path);
4877
+ addPathAliases(pathToResolvedId, file.path, resolvedId);
4878
+ }
4879
+ const facts = [];
4880
+ const tasks = [];
4881
+ const edges = [];
4882
+ let logContent = null;
4883
+ const logMdPath = findLogMdPath(files);
4884
+ for (const file of files) {
4885
+ if (file.path.endsWith("/log.md") || file.path === "log.md") {
4886
+ logContent = file.content;
4887
+ continue;
4888
+ }
4889
+ if (!isConceptFile(file.path)) continue;
4890
+ const { frontmatter, body } = coreOkf.parseConcept(file.content);
4891
+ const route = resolveRoute(file.path, frontmatter.type ?? "", options);
4892
+ if (route === "ignore") continue;
4893
+ const resolvedId = typeof frontmatter.id === "string" && frontmatter.id ? frontmatter.id : basenameMd(file.path);
4894
+ if (route === "fact") {
4895
+ facts.push(frontmatterToFact(entityId, resolvedId, frontmatter, body, now));
4896
+ } else {
4897
+ tasks.push(frontmatterToTask(entityId, resolvedId, frontmatter, now));
4898
+ }
4899
+ const seenEdges = /* @__PURE__ */ new Set();
4900
+ for (const link of coreOkf.extractMarkdownLinks(body)) {
4901
+ const strippedPath = stripLinkSuffix(link.path);
4902
+ const directTargetId = lookupResolvedId(pathToResolvedId, strippedPath);
4903
+ const resolvedTargetPath = resolveRelativePath(file.path, strippedPath);
4904
+ if (isStructuralPath(strippedPath) || isStructuralPath(resolvedTargetPath)) continue;
4905
+ const targetId = directTargetId ?? lookupResolvedId(pathToResolvedId, resolvedTargetPath);
4906
+ if (!targetId) continue;
4907
+ const edgeKey = `${resolvedId}\0${targetId}\0${link.text}`;
4908
+ if (seenEdges.has(edgeKey)) continue;
4909
+ seenEdges.add(edgeKey);
4910
+ edges.push({
4911
+ id: generateId(),
4912
+ entity_id: entityId,
4913
+ source_id: resolvedId,
4914
+ target_id: targetId,
4915
+ edge_type: link.text,
4916
+ created_at: now
4917
+ });
4918
+ }
4919
+ }
4920
+ const events = [];
4921
+ if (logContent != null) {
4922
+ const logPath = logMdPath ?? `entities/${entityId}/log.md`;
4923
+ for (const entry of coreOkf.parseLogMd(logContent)) {
4924
+ const parsed = parseLogEntryText(entry.text);
4925
+ if (!parsed) continue;
4926
+ let related_entry_id = null;
4927
+ if (parsed.linkPath) {
4928
+ const targetPath = resolveRelativePath(logPath, stripLinkSuffix(parsed.linkPath));
4929
+ if (!isStructuralPath(targetPath) && targetPath.includes("/facts/")) {
4930
+ related_entry_id = lookupResolvedId(pathToResolvedId, targetPath) ?? null;
4931
+ }
4932
+ }
4933
+ const created_at = (/* @__PURE__ */ new Date(`${entry.date}T00:00:00.000Z`)).getTime();
4934
+ if (!Number.isFinite(created_at)) continue;
4935
+ events.push({
4936
+ id: generateId("evt_"),
4937
+ entity_id: entityId,
4938
+ event_type: parsed.event_type,
4939
+ summary: parsed.summary,
4940
+ related_entry_id,
4941
+ created_at
4942
+ });
4943
+ }
4944
+ }
4945
+ return {
4946
+ generatedAt: now,
4947
+ entities: {
4948
+ [entityId]: { facts, tasks, events, edges }
4949
+ }
4950
+ };
4951
+ }
4557
4952
 
4558
4953
  // src/librarianPrompt.ts
4559
4954
  var DEFAULT_LIBRARIAN_SYNTHESIS_PROMPT = `You are a careful memory synthesis assistant.
@@ -4612,6 +5007,7 @@ exports.formatOkfBundle = formatOkfBundle;
4612
5007
  exports.hydrateLibrarianPrompt = hydrateLibrarianPrompt;
4613
5008
  exports.mapLibrarianOptionsToReadOptions = mapLibrarianOptionsToReadOptions;
4614
5009
  exports.parseEmbedding = parseEmbedding;
5010
+ exports.parseOkfBundle = parseOkfBundle;
4615
5011
  exports.validateLibrarianPromptTemplate = validateLibrarianPromptTemplate;
4616
5012
  //# sourceMappingURL=index.js.map
4617
5013
  //# sourceMappingURL=index.js.map