@equationalapplications/core-llm-wiki 7.7.5 → 7.7.7

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.
@@ -432,7 +432,31 @@ var _SearchService = class _SearchService {
432
432
  * auto-vacuum debt behind the TypeError in #64.
433
433
  */
434
434
  this.syncChain = Promise.resolve();
435
- this.miniSearch = new MiniSearch({
435
+ /**
436
+ * Entities whose index may have drifted from SQLite: an incremental update
437
+ * failed part-way, or core wrote rows it could not index (upsertGraph runs in
438
+ * the host's transaction). Their next syncEntries rebuilds the entity in full.
439
+ * See spec 2026-09-28 §5.
440
+ */
441
+ this.staleEntities = /* @__PURE__ */ new Set();
442
+ /**
443
+ * Per-entity count of markStale() calls. A rebuild clears an entity's stale
444
+ * flag only when the count it snapshotted before its read still holds
445
+ * afterwards: markStale() runs inside the host's still-open transaction, so
446
+ * a call landing mid-read belongs to rows the read cannot have seen, and
447
+ * its flag must outlive the turn (#233 review).
448
+ */
449
+ this.staleEpochs = /* @__PURE__ */ new Map();
450
+ this.miniSearch = this.createMiniSearch();
451
+ }
452
+ /**
453
+ * A fresh index with the production options. clearAll() swaps one in because
454
+ * MiniSearch.removeAll() empties the index but leaves dirtCount (and its
455
+ * vacuum bookkeeping) at its old value, which would trip syncEntries'
456
+ * conditional vacuum early after a clear.
457
+ */
458
+ createMiniSearch() {
459
+ return new MiniSearch({
436
460
  fields: ["title", "body", "tags"],
437
461
  storeFields: ["entity_id"],
438
462
  // Vacuuming is driven explicitly at the end of each serialized rebuild
@@ -459,7 +483,19 @@ var _SearchService = class _SearchService {
459
483
  const work = this.syncChain.then(async () => {
460
484
  try {
461
485
  try {
486
+ const epochsBefore = new Map(this.staleEpochs);
462
487
  await this.rebuildIndex(entityId);
488
+ if (entityId) {
489
+ if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
490
+ this.staleEntities.delete(entityId);
491
+ }
492
+ } else {
493
+ for (const id of [...this.staleEntities]) {
494
+ if ((this.staleEpochs.get(id) ?? 0) === (epochsBefore.get(id) ?? 0)) {
495
+ this.staleEntities.delete(id);
496
+ }
497
+ }
498
+ }
463
499
  await this.miniSearch.vacuum();
464
500
  } finally {
465
501
  this.evictCache(entityId);
@@ -471,6 +507,68 @@ var _SearchService = class _SearchService {
471
507
  this.syncChain = work;
472
508
  return work;
473
509
  }
510
+ /**
511
+ * Re-indexes only `ids` for `entityId`: drops each from the index, then
512
+ * re-adds the ones still live in SQLite, so soft-deleted or missing ids end
513
+ * up absent. Costs O(ids), where sync(entityId) costs O(entity) — callers that
514
+ * know what a write touched use this so chunked imports stay linear (#232).
515
+ *
516
+ * Serialized with sync() on the same chain and, like it, never rejects. An
517
+ * entity that is stale or has never been indexed gets a full rebuild instead
518
+ * — including on an empty `ids` list, which is a no-op only for an entity
519
+ * the index already tracks, and even then one that waits for rebuilds
520
+ * already queued on the chain.
521
+ */
522
+ async syncEntries(entityId, ids) {
523
+ const uniqueIds = [...new Set(ids)];
524
+ if (uniqueIds.length === 0 && !this.staleEntities.has(entityId) && this.miniSearchEntryIdsByEntity.has(entityId)) {
525
+ return this.syncChain;
526
+ }
527
+ const work = this.syncChain.then(async () => {
528
+ try {
529
+ try {
530
+ const epochsBefore = new Map(this.staleEpochs);
531
+ const needsRebuild = () => this.staleEntities.has(entityId) || !this.miniSearchEntryIdsByEntity.has(entityId);
532
+ if (!needsRebuild()) {
533
+ const rows = await this.entryRepo.findMiniSearchRowsByIds(entityId, uniqueIds);
534
+ const tracked = this.miniSearchEntryIdsByEntity.get(entityId);
535
+ if (tracked && !this.staleEntities.has(entityId)) {
536
+ for (const id of uniqueIds) {
537
+ if (tracked.delete(id) && this.miniSearch.has(id)) this.miniSearch.discard(id);
538
+ }
539
+ const documents = rows.map((row) => this.normalizeMiniSearchRow(row));
540
+ if (documents.length > 0) this.miniSearch.addAll(documents);
541
+ for (const document of documents) tracked.add(document.id);
542
+ if (this.miniSearch.dirtCount > 0) {
543
+ await this.miniSearch.vacuum();
544
+ }
545
+ return;
546
+ }
547
+ }
548
+ await this.rebuildIndex(entityId);
549
+ if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
550
+ this.staleEntities.delete(entityId);
551
+ }
552
+ await this.miniSearch.vacuum();
553
+ } finally {
554
+ this.evictCache(entityId);
555
+ }
556
+ } catch (err) {
557
+ this.staleEntities.add(entityId);
558
+ console.warn(`[WikiMemory] search index incremental sync failed for ${entityId}:`, err);
559
+ }
560
+ });
561
+ this.syncChain = work;
562
+ return work;
563
+ }
564
+ /**
565
+ * Forces the entity's next syncEntries to rebuild it in full. For writes core
566
+ * cannot index itself, such as upsertGraph inside a host transaction.
567
+ */
568
+ markStale(entityId) {
569
+ this.staleEntities.add(entityId);
570
+ this.staleEpochs.set(entityId, (this.staleEpochs.get(entityId) ?? 0) + 1);
571
+ }
474
572
  /**
475
573
  * Clears the parsed vector cache. Useful for mid-loop flush guarantees
476
574
  * or memory pressure evictions.
@@ -487,8 +585,10 @@ var _SearchService = class _SearchService {
487
585
  */
488
586
  clearAll() {
489
587
  this.vectorCache.clear();
490
- this.miniSearch.removeAll();
588
+ this.miniSearch = this.createMiniSearch();
491
589
  this.miniSearchEntryIdsByEntity.clear();
590
+ this.staleEntities.clear();
591
+ this.staleEpochs.clear();
492
592
  }
493
593
  /**
494
594
  * Executes a keyword search against the active MiniSearch index.
@@ -499,7 +599,7 @@ var _SearchService = class _SearchService {
499
599
  filter: (r) => entityIdSet.has(r.entity_id),
500
600
  combineWith: "OR"
501
601
  });
502
- return results.slice(0, limit);
602
+ return results.sort((a, b) => this._compareSearchResults(a, b)).slice(0, limit);
503
603
  }
504
604
  /**
505
605
  * Pre-fetches MiniSearch scores for candidate hydration, used during hybrid weighting.
@@ -509,7 +609,7 @@ var _SearchService = class _SearchService {
509
609
  let results = this.miniSearch.search(query, {
510
610
  filter: (r) => entityIdSet.has(r.entity_id),
511
611
  combineWith: "OR"
512
- });
612
+ }).sort((a, b) => this._compareSearchResults(a, b));
513
613
  if (preFilterLimit !== void 0) {
514
614
  results = results.slice(0, preFilterLimit);
515
615
  }
@@ -637,6 +737,18 @@ var _SearchService = class _SearchService {
637
737
  if (updatedAtDiff !== 0) return updatedAtDiff;
638
738
  return a.id.localeCompare(b.id);
639
739
  }
740
+ /**
741
+ * MiniSearch breaks equal-score ties by internal insertion order, which
742
+ * syncEntries changes: a discarded-and-re-added id moves to the end. Every
743
+ * caller truncates these results to a limit, so which ids survive a tie at
744
+ * the boundary must not depend on write history. Re-sort exact score ties
745
+ * by id — the same final tie-break _compareScoredRows applies.
746
+ */
747
+ _compareSearchResults(a, b) {
748
+ const scoreDiff = b.score - a.score;
749
+ if (!Number.isNaN(scoreDiff) && scoreDiff !== 0) return scoreDiff;
750
+ return a.id.localeCompare(b.id);
751
+ }
640
752
  };
641
753
  /**
642
754
  * Maximum number of entities whose parsed embedding vectors are held in
@@ -2290,7 +2402,10 @@ var IngestionService = class {
2290
2402
  return zeroChunkResult(canonical);
2291
2403
  }
2292
2404
  diagBuffer.flush(this.options);
2293
- await this.searchService.sync(entityId);
2405
+ await this.searchService.syncEntries(
2406
+ entityId,
2407
+ [...deletedSourceFactIds, ...insertedFacts.map((fact) => fact.id)]
2408
+ );
2294
2409
  if (failedChunks === 0) {
2295
2410
  const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
2296
2411
  for (const factId of uniqueDeletedSourceFactIds) {
@@ -3118,6 +3233,7 @@ var MaintenanceService = class {
3118
3233
  this._validatePruneDuration(retainSoftDeletedFor, "retainSoftDeletedFor");
3119
3234
  this._validatePruneDuration(retainEventsFor, "retainEventsFor");
3120
3235
  const now = Date.now();
3236
+ let syncedIds = [];
3121
3237
  let deletedEntries = 0;
3122
3238
  let deletedTasks = 0;
3123
3239
  let deletedEvents = 0;
@@ -3136,6 +3252,7 @@ var MaintenanceService = class {
3136
3252
  }
3137
3253
  }
3138
3254
  const succeededIds = succeeded.map((r) => r.id);
3255
+ syncedIds = succeededIds;
3139
3256
  await this.db.withTransactionAsync(async (tx) => {
3140
3257
  if (succeededIds.length > 0) {
3141
3258
  deletedEntries = await this.entryRepo.bulkDeletePruned(entityId, cutoff, succeededIds, tx);
@@ -3143,7 +3260,7 @@ var MaintenanceService = class {
3143
3260
  deletedTasks = await this.taskRepo.bulkDeletePruned(entityId, cutoff, tx);
3144
3261
  });
3145
3262
  if (failure) {
3146
- await this.searchService.sync(entityId);
3263
+ await this.searchService.syncEntries(entityId, syncedIds);
3147
3264
  const remaining = entriesToDelete.length - succeeded.length - 1;
3148
3265
  const isTimeout = failure.cause?.[HOOK_TIMEOUT_MARKER] === true;
3149
3266
  if (isTimeout) {
@@ -3177,7 +3294,7 @@ var MaintenanceService = class {
3177
3294
  if (vacuum) {
3178
3295
  await this.metadataRepo.vacuum();
3179
3296
  }
3180
- await this.searchService.sync(entityId);
3297
+ await this.searchService.syncEntries(entityId, syncedIds);
3181
3298
  return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
3182
3299
  } finally {
3183
3300
  this.jobManager.releaseLock("prune", entityId);
@@ -3347,8 +3464,8 @@ var MaintenanceService = class {
3347
3464
  deletedEntries += refResult;
3348
3465
  }
3349
3466
  });
3350
- await this.searchService.sync(entityId);
3351
3467
  const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
3468
+ await this.searchService.syncEntries(entityId, uniqueDeletedIds);
3352
3469
  for (const factId of uniqueDeletedIds) {
3353
3470
  try {
3354
3471
  await this.embeddingService.notifyEmbeddingPersistedOrThrow(entityId, factId, null);
@@ -3546,7 +3663,7 @@ var MaintenanceService = class {
3546
3663
  }
3547
3664
  });
3548
3665
  diagBuffer.flush(this.options);
3549
- await this.searchService.sync(entityId);
3666
+ await this.searchService.syncEntries(entityId, insertedFacts.map((f) => f.id));
3550
3667
  for (const fact of insertedFacts) {
3551
3668
  await this.embeddingService.embedFact(fact, { operation: "librarian", trigger });
3552
3669
  }
@@ -3604,7 +3721,7 @@ var MaintenanceService = class {
3604
3721
  }
3605
3722
  const healCandidates = await this.entryRepo.findHealCandidatesByEntityId(entityId, batchSize, recheckCutoff);
3606
3723
  if (healCandidates.length === 0) {
3607
- await this.searchService.sync(entityId);
3724
+ await this.searchService.syncEntries(entityId, Array.from(/* @__PURE__ */ new Set([...orphanedIds, ...staleDowngradedIds])));
3608
3725
  this.searchService.evictCache(entityId);
3609
3726
  const counts2 = await this.entryRepo.countHealCandidatesByEntityId(entityId, recheckCutoff);
3610
3727
  return {
@@ -3774,7 +3891,14 @@ var MaintenanceService = class {
3774
3891
  diagBuffer.push({ ...diagBase, code: "heal_skipped", detail: { factId: item.id, reason } });
3775
3892
  }
3776
3893
  diagBuffer.flush(this.options);
3777
- await this.searchService.sync(entityId);
3894
+ const healSyncIds = Array.from(/* @__PURE__ */ new Set([
3895
+ ...orphanedIds,
3896
+ ...staleDowngradedIds,
3897
+ ...safeDowngraded,
3898
+ ...uniqueDeletedFactIds,
3899
+ ...insertedFacts.map((f) => f.id)
3900
+ ]));
3901
+ await this.searchService.syncEntries(entityId, healSyncIds);
3778
3902
  for (const factId of uniqueDeletedFactIds) {
3779
3903
  try {
3780
3904
  await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
@@ -4479,7 +4603,11 @@ var ImportExportService = class {
4479
4603
  );
4480
4604
  }
4481
4605
  });
4482
- await this.searchService.sync(entityId);
4606
+ if (merge) {
4607
+ await this.searchService.syncEntries(entityId, [...upsertedFactIds]);
4608
+ } else {
4609
+ await this.searchService.sync(entityId);
4610
+ }
4483
4611
  for (const fact of bundle.facts) {
4484
4612
  if (!fact.deleted_at && upsertedFactIds.has(fact.id) && !factsWithPreservedBlob.has(fact.id)) {
4485
4613
  const clipped = clippedTextByFactId.get(fact.id);
@@ -5663,5 +5791,5 @@ var WriteService = class {
5663
5791
  };
5664
5792
 
5665
5793
  export { BaseRepository, DEFAULT_CHUNK_OVERLAP, DEFAULT_MAX_CHUNK_LENGTH, DEFAULT_MAX_EMBED_CHARS, DiagnosticBuffer, EMBED_CHARS_CEILING, EmbeddingService, HEAL_ANCHORS_PER_CANDIDATE, HEAL_BATCH_SIZE, HEAL_MAX_FACT_BODY_CHARS_L3, HEAL_MAX_TASKS, HEAL_RECHECK_MS, HOOK_TIMEOUT_MARKER, ImportExportService, IngestionService, JobManager, MaintenanceService, MetadataRepository, ONTOLOGY_BACKFILL_BATCH_SIZE, ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, ONTOLOGY_BACKFILL_RECHECK_MS, ONTOLOGY_BACKFILL_SYSTEM_PROMPT, PromptService, PrunePartialFailureError, RetrievalService, SearchService, WikiBusyError, WikiDraftNotFound, WikiDuplicateHashError, WikiGraphNodeOwnershipConflict, WikiIngestEmptyError, WikiInvalidReadOptions, WikiParseError, WikiSourceRefHashCollision, WikiStrictOntologyViolation, WikiTransactionError, WriteService, __privateAdd, __privateGet, __privateSet, chunkText, configureRandomSource, edgeTripleAllowed, emptyManifest, entitySummaryMetaKey, extractSqliteCode, generateId, normalizeSourceHash, normalizeSourceRef, normalizeTitleKey, parseEmbedding, resolveEdgeDefinitions, resolveGrounding, resolveNodeType, safeSlice, typeSatisfies, validateInlineEdges, validateManifest };
5666
- //# sourceMappingURL=chunk-JV2OQ5CM.mjs.map
5667
- //# sourceMappingURL=chunk-JV2OQ5CM.mjs.map
5794
+ //# sourceMappingURL=chunk-46X3HO77.mjs.map
5795
+ //# sourceMappingURL=chunk-46X3HO77.mjs.map