@equationalapplications/core-llm-wiki 7.8.0 → 7.9.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/README.md +9 -0
- package/dist/{chunk-M76CF2Q6.mjs → chunk-7FRLQ7ZO.mjs} +83 -129
- package/dist/chunk-7FRLQ7ZO.mjs.map +1 -0
- package/dist/index.d.mts +2 -3
- package/dist/index.d.ts +2 -3
- package/dist/index.js +305 -130
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +227 -6
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-CWZaXKLj.d.mts → testing-Bf6etdUy.d.mts} +156 -26
- package/dist/{testing-CWZaXKLj.d.ts → testing-Bf6etdUy.d.ts} +156 -26
- package/dist/testing.d.mts +1 -2
- package/dist/testing.d.ts +1 -2
- package/dist/testing.js +83 -131
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +3 -3
- package/dist/chunk-M76CF2Q6.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-
|
|
2
|
-
export { C as ChunkFailure, d as ClassifierAnswer, e as ClassifierQuestion, f as ClassifyRequest, g as ClassifyResponse, D as DegradedRecord, h as DraftPage, E as EmbedFactResult, i as EmbedFailureKind, j as EmbeddingMarkerKind, k as EntityMaintenanceReport, l as EntityStatus, m as ExtractedFact, n as ExtractedFactEdge, o as ExtractedFactWithOntology, p as ExtractedTask, q as GraphTraversalOptions, r as GroundingConfig, H as HEAL_BATCH_SIZE, s as HEAL_RECHECK_MS, t as HOOK_TIMEOUT_MARKER, u as HealResult, I as IngestDocumentResult, L as LLMProvider, v as LibrarianConfig, w as LibrarianGateConfig, x as MaintenanceJob, y as MaintenanceStopReason, O as ONTOLOGY_BACKFILL_BATCH_SIZE, z as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, A as ONTOLOGY_BACKFILL_RECHECK_MS, B as OntologyBackfillResult, J as OntologyConfig, K as OntologyEdgeType, N as OntologyManifest, P as OntologyMode, Q as OntologyNodeType, T as OntologyPromptContext, U as OntologyUpdates, V as PendingMaintenance, X as PendingSourceStatus, Y as PromptOverrides, Z as PromptService, _ as PrunePartialFailureError, $ as ReembedResult, a0 as RunPendingMaintenanceOptions, a1 as RunPendingMaintenanceResult, a2 as SupersedeReplacement, a3 as VectorRanker, a4 as VectorRankerFallback, a5 as VectorRankerRankArgs, a6 as VectorRankerSemanticResult, a7 as WikiBusyError, a8 as WikiBusyOperation, a9 as WikiCheckpoint, aa as WikiConfig, ab as WikiDiagnostic, ac as WikiDiagnosticCode, ad as WikiDiagnosticDetail, ae as WikiDiagnosticOperation, af as WikiDiagnosticSeverity, ag as WikiDiagnosticTrigger, ah as WikiDraftNotFound, ai as WikiDuplicateHashError, aj as WikiEdge, ak as WikiEvent, al as WikiFact, am as WikiGraphNodeOwnershipConflict, an as WikiIngestEmptyError, ao as WikiInstructions, ap as WikiInvalidReadOptions, aq as WikiLintReport, ar as WikiMemoryTestAccess, as as WikiOutboxEvent, at as WikiParseError, au as WikiSourceRefHashCollision, av as WikiStrictOntologyViolation, aw as WikiSupersedeError, ax as WikiSupersedeReason, ay as WikiTask, az as WikiTransactionError, aA as validateManifest } from './testing-
|
|
1
|
+
import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-Bf6etdUy.mjs';
|
|
2
|
+
export { C as ChunkFailure, d as ClassifierAnswer, e as ClassifierQuestion, f as ClassifyRequest, g as ClassifyResponse, D as DegradedRecord, h as DraftPage, E as EmbedFactResult, i as EmbedFailureKind, j as EmbeddingMarkerKind, k as EntityMaintenanceReport, l as EntityStatus, m as ExtractedFact, n as ExtractedFactEdge, o as ExtractedFactWithOntology, p as ExtractedTask, q as GraphTraversalOptions, r as GroundingConfig, H as HEAL_BATCH_SIZE, s as HEAL_RECHECK_MS, t as HOOK_TIMEOUT_MARKER, u as HealResult, I as IngestDocumentResult, L as LLMProvider, v as LibrarianConfig, w as LibrarianGateConfig, x as MaintenanceJob, y as MaintenanceStopReason, O as ONTOLOGY_BACKFILL_BATCH_SIZE, z as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, A as ONTOLOGY_BACKFILL_RECHECK_MS, B as OntologyBackfillResult, J as OntologyConfig, K as OntologyEdgeType, N as OntologyManifest, P as OntologyMode, Q as OntologyNodeType, T as OntologyPromptContext, U as OntologyUpdates, V as PendingMaintenance, X as PendingSourceStatus, Y as PromptOverrides, Z as PromptService, _ as PrunePartialFailureError, $ as ReembedResult, a0 as RunPendingMaintenanceOptions, a1 as RunPendingMaintenanceResult, a2 as SupersedeReplacement, a3 as VectorRanker, a4 as VectorRankerFallback, a5 as VectorRankerRankArgs, a6 as VectorRankerSemanticResult, a7 as WikiBusyError, a8 as WikiBusyOperation, a9 as WikiCheckpoint, aa as WikiConfig, ab as WikiDiagnostic, ac as WikiDiagnosticCode, ad as WikiDiagnosticDetail, ae as WikiDiagnosticOperation, af as WikiDiagnosticSeverity, ag as WikiDiagnosticTrigger, ah as WikiDraftNotFound, ai as WikiDuplicateHashError, aj as WikiEdge, ak as WikiEvent, al as WikiFact, am as WikiGraphNodeOwnershipConflict, an as WikiIngestEmptyError, ao as WikiInstructions, ap as WikiInvalidReadOptions, aq as WikiLintReport, ar as WikiMemoryTestAccess, as as WikiOutboxEvent, at as WikiParseError, au as WikiSourceRefHashCollision, av as WikiStrictOntologyViolation, aw as WikiSupersedeError, ax as WikiSupersedeReason, ay as WikiTask, az as WikiTransactionError, aA as validateManifest } from './testing-Bf6etdUy.mjs';
|
|
3
3
|
import { OkfFile } from '@equationalapplications/core-okf';
|
|
4
|
-
import 'minisearch';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Slices a string like `String.prototype.slice`, but clamps out-of-range
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-
|
|
2
|
-
export { C as ChunkFailure, d as ClassifierAnswer, e as ClassifierQuestion, f as ClassifyRequest, g as ClassifyResponse, D as DegradedRecord, h as DraftPage, E as EmbedFactResult, i as EmbedFailureKind, j as EmbeddingMarkerKind, k as EntityMaintenanceReport, l as EntityStatus, m as ExtractedFact, n as ExtractedFactEdge, o as ExtractedFactWithOntology, p as ExtractedTask, q as GraphTraversalOptions, r as GroundingConfig, H as HEAL_BATCH_SIZE, s as HEAL_RECHECK_MS, t as HOOK_TIMEOUT_MARKER, u as HealResult, I as IngestDocumentResult, L as LLMProvider, v as LibrarianConfig, w as LibrarianGateConfig, x as MaintenanceJob, y as MaintenanceStopReason, O as ONTOLOGY_BACKFILL_BATCH_SIZE, z as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, A as ONTOLOGY_BACKFILL_RECHECK_MS, B as OntologyBackfillResult, J as OntologyConfig, K as OntologyEdgeType, N as OntologyManifest, P as OntologyMode, Q as OntologyNodeType, T as OntologyPromptContext, U as OntologyUpdates, V as PendingMaintenance, X as PendingSourceStatus, Y as PromptOverrides, Z as PromptService, _ as PrunePartialFailureError, $ as ReembedResult, a0 as RunPendingMaintenanceOptions, a1 as RunPendingMaintenanceResult, a2 as SupersedeReplacement, a3 as VectorRanker, a4 as VectorRankerFallback, a5 as VectorRankerRankArgs, a6 as VectorRankerSemanticResult, a7 as WikiBusyError, a8 as WikiBusyOperation, a9 as WikiCheckpoint, aa as WikiConfig, ab as WikiDiagnostic, ac as WikiDiagnosticCode, ad as WikiDiagnosticDetail, ae as WikiDiagnosticOperation, af as WikiDiagnosticSeverity, ag as WikiDiagnosticTrigger, ah as WikiDraftNotFound, ai as WikiDuplicateHashError, aj as WikiEdge, ak as WikiEvent, al as WikiFact, am as WikiGraphNodeOwnershipConflict, an as WikiIngestEmptyError, ao as WikiInstructions, ap as WikiInvalidReadOptions, aq as WikiLintReport, ar as WikiMemoryTestAccess, as as WikiOutboxEvent, at as WikiParseError, au as WikiSourceRefHashCollision, av as WikiStrictOntologyViolation, aw as WikiSupersedeError, ax as WikiSupersedeReason, ay as WikiTask, az as WikiTransactionError, aA as validateManifest } from './testing-
|
|
1
|
+
import { M as MemoryBundle, F as FormatContextOptions, G as GraphNeighborhood, a as MemoryDump, b as FormattedMemoryDump, R as ReadOptions, S as SQLiteAdapter, W as WikiOptions, c as WikiMemory } from './testing-Bf6etdUy.js';
|
|
2
|
+
export { C as ChunkFailure, d as ClassifierAnswer, e as ClassifierQuestion, f as ClassifyRequest, g as ClassifyResponse, D as DegradedRecord, h as DraftPage, E as EmbedFactResult, i as EmbedFailureKind, j as EmbeddingMarkerKind, k as EntityMaintenanceReport, l as EntityStatus, m as ExtractedFact, n as ExtractedFactEdge, o as ExtractedFactWithOntology, p as ExtractedTask, q as GraphTraversalOptions, r as GroundingConfig, H as HEAL_BATCH_SIZE, s as HEAL_RECHECK_MS, t as HOOK_TIMEOUT_MARKER, u as HealResult, I as IngestDocumentResult, L as LLMProvider, v as LibrarianConfig, w as LibrarianGateConfig, x as MaintenanceJob, y as MaintenanceStopReason, O as ONTOLOGY_BACKFILL_BATCH_SIZE, z as ONTOLOGY_BACKFILL_MAX_PROMPT_CHARS, A as ONTOLOGY_BACKFILL_RECHECK_MS, B as OntologyBackfillResult, J as OntologyConfig, K as OntologyEdgeType, N as OntologyManifest, P as OntologyMode, Q as OntologyNodeType, T as OntologyPromptContext, U as OntologyUpdates, V as PendingMaintenance, X as PendingSourceStatus, Y as PromptOverrides, Z as PromptService, _ as PrunePartialFailureError, $ as ReembedResult, a0 as RunPendingMaintenanceOptions, a1 as RunPendingMaintenanceResult, a2 as SupersedeReplacement, a3 as VectorRanker, a4 as VectorRankerFallback, a5 as VectorRankerRankArgs, a6 as VectorRankerSemanticResult, a7 as WikiBusyError, a8 as WikiBusyOperation, a9 as WikiCheckpoint, aa as WikiConfig, ab as WikiDiagnostic, ac as WikiDiagnosticCode, ad as WikiDiagnosticDetail, ae as WikiDiagnosticOperation, af as WikiDiagnosticSeverity, ag as WikiDiagnosticTrigger, ah as WikiDraftNotFound, ai as WikiDuplicateHashError, aj as WikiEdge, ak as WikiEvent, al as WikiFact, am as WikiGraphNodeOwnershipConflict, an as WikiIngestEmptyError, ao as WikiInstructions, ap as WikiInvalidReadOptions, aq as WikiLintReport, ar as WikiMemoryTestAccess, as as WikiOutboxEvent, at as WikiParseError, au as WikiSourceRefHashCollision, av as WikiStrictOntologyViolation, aw as WikiSupersedeError, ax as WikiSupersedeReason, ay as WikiTask, az as WikiTransactionError, aA as validateManifest } from './testing-Bf6etdUy.js';
|
|
3
3
|
import { OkfFile } from '@equationalapplications/core-okf';
|
|
4
|
-
import 'minisearch';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Slices a string like `String.prototype.slice`, but clamps out-of-range
|
package/dist/index.js
CHANGED
|
@@ -3469,11 +3469,28 @@ function parseEmbedding(blob, text) {
|
|
|
3469
3469
|
return null;
|
|
3470
3470
|
}
|
|
3471
3471
|
|
|
3472
|
+
// src/utils/indexDoc.ts
|
|
3473
|
+
function toIndexDoc(row) {
|
|
3474
|
+
return {
|
|
3475
|
+
id: row.id,
|
|
3476
|
+
entity_id: row.entity_id,
|
|
3477
|
+
title: row.title,
|
|
3478
|
+
body: row.body,
|
|
3479
|
+
tags: (() => {
|
|
3480
|
+
try {
|
|
3481
|
+
const parsed = JSON.parse(row.tags);
|
|
3482
|
+
return Array.isArray(parsed) ? parsed.join(" ") : row.tags;
|
|
3483
|
+
} catch {
|
|
3484
|
+
return row.tags;
|
|
3485
|
+
}
|
|
3486
|
+
})()
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3472
3490
|
// src/services/SearchService.ts
|
|
3473
3491
|
var _SearchService = class _SearchService {
|
|
3474
|
-
constructor(entryRepo) {
|
|
3492
|
+
constructor(entryRepo, indexStrategy) {
|
|
3475
3493
|
this.entryRepo = entryRepo;
|
|
3476
|
-
this.miniSearchEntryIdsByEntity = /* @__PURE__ */ new Map();
|
|
3477
3494
|
this.vectorCache = /* @__PURE__ */ new Map();
|
|
3478
3495
|
/**
|
|
3479
3496
|
* Serializes rebuilds. `rebuildIndex` awaits a repository read between
|
|
@@ -3499,35 +3516,13 @@ var _SearchService = class _SearchService {
|
|
|
3499
3516
|
* its flag must outlive the turn (#233 review).
|
|
3500
3517
|
*/
|
|
3501
3518
|
this.staleEpochs = /* @__PURE__ */ new Map();
|
|
3502
|
-
this.
|
|
3503
|
-
}
|
|
3504
|
-
/**
|
|
3505
|
-
* A fresh index with the production options. clearAll() swaps one in because
|
|
3506
|
-
* MiniSearch.removeAll() empties the index but leaves dirtCount (and its
|
|
3507
|
-
* vacuum bookkeeping) at its old value, which would trip syncEntries'
|
|
3508
|
-
* conditional vacuum early after a clear.
|
|
3509
|
-
*/
|
|
3510
|
-
createMiniSearch() {
|
|
3511
|
-
return new MiniSearch__default.default({
|
|
3512
|
-
fields: ["title", "body", "tags"],
|
|
3513
|
-
storeFields: ["entity_id"],
|
|
3514
|
-
// Vacuuming is driven explicitly at the end of each serialized rebuild
|
|
3515
|
-
// (see sync). Auto-vacuum fires on its own schedule, asynchronously with
|
|
3516
|
-
// respect to the caller, and traversing the tree mid-rebuild is what
|
|
3517
|
-
// threw the uncaught TypeError in MiniSearch.performVacuuming (#64).
|
|
3518
|
-
autoVacuum: false,
|
|
3519
|
-
searchOptions: {
|
|
3520
|
-
boost: { title: 2 },
|
|
3521
|
-
fuzzy: 0.2,
|
|
3522
|
-
prefix: true
|
|
3523
|
-
}
|
|
3524
|
-
});
|
|
3519
|
+
this.indexStrategy = indexStrategy;
|
|
3525
3520
|
}
|
|
3526
3521
|
/**
|
|
3527
3522
|
* Rebuilds the search index and clears the vector cache for a given entity.
|
|
3528
3523
|
* A direct replacement for manually syncing state after a DB transaction.
|
|
3529
3524
|
*
|
|
3530
|
-
* Rebuilds are serialized per instance and never reject: the
|
|
3525
|
+
* Rebuilds are serialized per instance and never reject: the keyword index
|
|
3531
3526
|
* is a rebuildable cache over SQLite, so degraded keyword search is the
|
|
3532
3527
|
* correct failure mode and killing the host process is not.
|
|
3533
3528
|
*/
|
|
@@ -3536,19 +3531,21 @@ var _SearchService = class _SearchService {
|
|
|
3536
3531
|
try {
|
|
3537
3532
|
try {
|
|
3538
3533
|
const epochsBefore = new Map(this.staleEpochs);
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
this.staleEntities.delete(entityId);
|
|
3543
|
-
}
|
|
3544
|
-
} else {
|
|
3534
|
+
if (entityId === void 0) {
|
|
3535
|
+
const rows = await this.entryRepo.findMiniSearchRows();
|
|
3536
|
+
await this.indexStrategy.replaceAll(rows.map(toIndexDoc));
|
|
3545
3537
|
for (const id of [...this.staleEntities]) {
|
|
3546
3538
|
if ((this.staleEpochs.get(id) ?? 0) === (epochsBefore.get(id) ?? 0)) {
|
|
3547
3539
|
this.staleEntities.delete(id);
|
|
3548
3540
|
}
|
|
3549
3541
|
}
|
|
3542
|
+
} else {
|
|
3543
|
+
await this.rebuildIndex(entityId);
|
|
3544
|
+
if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
|
|
3545
|
+
this.staleEntities.delete(entityId);
|
|
3546
|
+
}
|
|
3550
3547
|
}
|
|
3551
|
-
await this.
|
|
3548
|
+
await this.indexStrategy.vacuum?.();
|
|
3552
3549
|
} finally {
|
|
3553
3550
|
this.evictCache(entityId);
|
|
3554
3551
|
}
|
|
@@ -3573,27 +3570,20 @@ var _SearchService = class _SearchService {
|
|
|
3573
3570
|
*/
|
|
3574
3571
|
async syncEntries(entityId, ids) {
|
|
3575
3572
|
const uniqueIds = [...new Set(ids)];
|
|
3576
|
-
|
|
3573
|
+
const indexed = this.indexStrategy.hasIndexedEntity?.(entityId);
|
|
3574
|
+
if (uniqueIds.length === 0 && !this.staleEntities.has(entityId) && indexed === true) {
|
|
3577
3575
|
return this.syncChain;
|
|
3578
3576
|
}
|
|
3579
3577
|
const work = this.syncChain.then(async () => {
|
|
3580
3578
|
try {
|
|
3581
3579
|
try {
|
|
3582
3580
|
const epochsBefore = new Map(this.staleEpochs);
|
|
3583
|
-
const needsRebuild = () => this.staleEntities.has(entityId) ||
|
|
3581
|
+
const needsRebuild = () => this.staleEntities.has(entityId) || this.indexStrategy.hasIndexedEntity?.(entityId) !== true;
|
|
3584
3582
|
if (!needsRebuild()) {
|
|
3585
3583
|
const rows = await this.entryRepo.findMiniSearchRowsByIds(entityId, uniqueIds);
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
if (tracked.delete(id) && this.miniSearch.has(id)) this.miniSearch.discard(id);
|
|
3590
|
-
}
|
|
3591
|
-
const documents = rows.map((row) => this.normalizeMiniSearchRow(row));
|
|
3592
|
-
if (documents.length > 0) this.miniSearch.addAll(documents);
|
|
3593
|
-
for (const document of documents) tracked.add(document.id);
|
|
3594
|
-
if (this.miniSearch.dirtCount > 0) {
|
|
3595
|
-
await this.miniSearch.vacuum();
|
|
3596
|
-
}
|
|
3584
|
+
if (!this.staleEntities.has(entityId)) {
|
|
3585
|
+
await this.indexStrategy.replace(entityId, uniqueIds, rows.map(toIndexDoc));
|
|
3586
|
+
await this.indexStrategy.vacuum?.();
|
|
3597
3587
|
return;
|
|
3598
3588
|
}
|
|
3599
3589
|
}
|
|
@@ -3601,7 +3591,7 @@ var _SearchService = class _SearchService {
|
|
|
3601
3591
|
if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
|
|
3602
3592
|
this.staleEntities.delete(entityId);
|
|
3603
3593
|
}
|
|
3604
|
-
await this.
|
|
3594
|
+
await this.indexStrategy.vacuum?.();
|
|
3605
3595
|
} finally {
|
|
3606
3596
|
this.evictCache(entityId);
|
|
3607
3597
|
}
|
|
@@ -3621,6 +3611,16 @@ var _SearchService = class _SearchService {
|
|
|
3621
3611
|
this.staleEntities.add(entityId);
|
|
3622
3612
|
this.staleEpochs.set(entityId, (this.staleEpochs.get(entityId) ?? 0) + 1);
|
|
3623
3613
|
}
|
|
3614
|
+
/**
|
|
3615
|
+
* Runs syncEntries(id, []) for each entity marked stale when this is called,
|
|
3616
|
+
* so only those entities are rebuilt. Each rebuild still takes its own turn
|
|
3617
|
+
* on the sync chain, and like syncEntries this never rejects. With nothing
|
|
3618
|
+
* stale it resolves at once, without waiting on work already on the chain.
|
|
3619
|
+
*/
|
|
3620
|
+
async syncStale() {
|
|
3621
|
+
const ids = [...this.staleEntities];
|
|
3622
|
+
await Promise.all(ids.map((id) => this.syncEntries(id, [])));
|
|
3623
|
+
}
|
|
3624
3624
|
/**
|
|
3625
3625
|
* Clears the parsed vector cache. Useful for mid-loop flush guarantees
|
|
3626
3626
|
* or memory pressure evictions.
|
|
@@ -3635,39 +3635,28 @@ var _SearchService = class _SearchService {
|
|
|
3635
3635
|
/**
|
|
3636
3636
|
* Fully resets the search service.
|
|
3637
3637
|
*/
|
|
3638
|
-
clearAll() {
|
|
3638
|
+
async clearAll() {
|
|
3639
3639
|
this.vectorCache.clear();
|
|
3640
|
-
|
|
3641
|
-
this.miniSearchEntryIdsByEntity.clear();
|
|
3640
|
+
await this.indexStrategy.replaceAll([]);
|
|
3642
3641
|
this.staleEntities.clear();
|
|
3643
3642
|
this.staleEpochs.clear();
|
|
3644
3643
|
}
|
|
3645
3644
|
/**
|
|
3646
|
-
* Executes a keyword search against the active
|
|
3645
|
+
* Executes a keyword search against the active index strategy.
|
|
3647
3646
|
*/
|
|
3648
|
-
searchKeyword(query, entityIds, limit) {
|
|
3649
|
-
const
|
|
3650
|
-
const results = this.miniSearch.search(query, {
|
|
3651
|
-
filter: (r) => entityIdSet.has(r.entity_id),
|
|
3652
|
-
combineWith: "OR"
|
|
3653
|
-
});
|
|
3647
|
+
async searchKeyword(query, entityIds, limit) {
|
|
3648
|
+
const results = finiteResults(await this.indexStrategy.search(query, { entityIds, limit }));
|
|
3654
3649
|
return results.sort((a, b) => this._compareSearchResults(a, b)).slice(0, limit);
|
|
3655
3650
|
}
|
|
3656
3651
|
/**
|
|
3657
|
-
* Pre-fetches
|
|
3652
|
+
* Pre-fetches keyword scores for candidate hydration, used during hybrid weighting.
|
|
3658
3653
|
*/
|
|
3659
|
-
|
|
3660
|
-
const
|
|
3661
|
-
|
|
3662
|
-
filter: (r) => entityIdSet.has(r.entity_id),
|
|
3663
|
-
combineWith: "OR"
|
|
3664
|
-
}).sort((a, b) => this._compareSearchResults(a, b));
|
|
3665
|
-
if (preFilterLimit !== void 0) {
|
|
3666
|
-
results = results.slice(0, preFilterLimit);
|
|
3667
|
-
}
|
|
3654
|
+
async getKeywordScores(query, entityIds, preFilterLimit) {
|
|
3655
|
+
const opts = preFilterLimit !== void 0 ? { entityIds, limit: preFilterLimit, preFilterLimit } : { entityIds, limit: Number.MAX_SAFE_INTEGER };
|
|
3656
|
+
const results = finiteResults(await this.indexStrategy.search(query, opts));
|
|
3668
3657
|
if (results.length === 0) return /* @__PURE__ */ new Map();
|
|
3669
|
-
const
|
|
3670
|
-
return new Map(results.map((r) => [r.id, r.score /
|
|
3658
|
+
const maxScore = Math.max(1, results[0]?.score ?? 1);
|
|
3659
|
+
return new Map(results.map((r) => [r.id, r.score / maxScore]));
|
|
3671
3660
|
}
|
|
3672
3661
|
/**
|
|
3673
3662
|
* Score candidate rows using in-process JS cosine similarity.
|
|
@@ -3729,53 +3718,16 @@ var _SearchService = class _SearchService {
|
|
|
3729
3718
|
return scored.slice(0, limit);
|
|
3730
3719
|
}
|
|
3731
3720
|
// --- Internal Index Management ---
|
|
3721
|
+
/**
|
|
3722
|
+
* Rebuild a single entity's index from scratch. Reads every row for
|
|
3723
|
+
* `entityId` from the repository (already filtered by `deleted_at IS NULL`)
|
|
3724
|
+
* and hands the lot to `indexStrategy.replaceEntity`, which owns the
|
|
3725
|
+
* drop-then-add bookkeeping so callers don't need to know the strategy's
|
|
3726
|
+
* id-tracking shape.
|
|
3727
|
+
*/
|
|
3732
3728
|
async rebuildIndex(entityId) {
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
const previousIds = this.miniSearchEntryIdsByEntity.get(entityId);
|
|
3736
|
-
if (previousIds) {
|
|
3737
|
-
for (const id of previousIds) {
|
|
3738
|
-
this.miniSearch.discard(id);
|
|
3739
|
-
}
|
|
3740
|
-
}
|
|
3741
|
-
const documents2 = rows2.map((row) => this.normalizeMiniSearchRow(row));
|
|
3742
|
-
if (documents2.length > 0) {
|
|
3743
|
-
this.miniSearch.addAll(documents2);
|
|
3744
|
-
}
|
|
3745
|
-
this.miniSearchEntryIdsByEntity.set(
|
|
3746
|
-
entityId,
|
|
3747
|
-
new Set(documents2.map((document) => document.id))
|
|
3748
|
-
);
|
|
3749
|
-
return;
|
|
3750
|
-
}
|
|
3751
|
-
const rows = await this.entryRepo.findMiniSearchRows();
|
|
3752
|
-
this.miniSearch.removeAll();
|
|
3753
|
-
this.miniSearchEntryIdsByEntity.clear();
|
|
3754
|
-
const documents = rows.map((row) => this.normalizeMiniSearchRow(row));
|
|
3755
|
-
if (documents.length > 0) {
|
|
3756
|
-
this.miniSearch.addAll(documents);
|
|
3757
|
-
}
|
|
3758
|
-
for (const document of documents) {
|
|
3759
|
-
const ids = this.miniSearchEntryIdsByEntity.get(document.entity_id) ?? /* @__PURE__ */ new Set();
|
|
3760
|
-
ids.add(document.id);
|
|
3761
|
-
this.miniSearchEntryIdsByEntity.set(document.entity_id, ids);
|
|
3762
|
-
}
|
|
3763
|
-
}
|
|
3764
|
-
normalizeMiniSearchRow(row) {
|
|
3765
|
-
return {
|
|
3766
|
-
id: row.id,
|
|
3767
|
-
entity_id: row.entity_id,
|
|
3768
|
-
title: row.title,
|
|
3769
|
-
body: row.body,
|
|
3770
|
-
tags: (() => {
|
|
3771
|
-
try {
|
|
3772
|
-
const parsed = JSON.parse(row.tags);
|
|
3773
|
-
return Array.isArray(parsed) ? parsed.join(" ") : row.tags;
|
|
3774
|
-
} catch {
|
|
3775
|
-
return row.tags;
|
|
3776
|
-
}
|
|
3777
|
-
})()
|
|
3778
|
-
};
|
|
3729
|
+
const rows = await this.entryRepo.findMiniSearchRows(entityId);
|
|
3730
|
+
await this.indexStrategy.replaceEntity(entityId, rows.map(toIndexDoc));
|
|
3779
3731
|
}
|
|
3780
3732
|
_tieBreakSort(items) {
|
|
3781
3733
|
items.sort((a, b) => this._compareScoredRows(a, b));
|
|
@@ -3790,11 +3742,11 @@ var _SearchService = class _SearchService {
|
|
|
3790
3742
|
return a.id.localeCompare(b.id);
|
|
3791
3743
|
}
|
|
3792
3744
|
/**
|
|
3793
|
-
*
|
|
3794
|
-
* syncEntries changes: a discarded-and-re-added id moves to the end.
|
|
3795
|
-
* caller truncates these results to a limit, so which ids survive a
|
|
3796
|
-
* the boundary must not depend on write history. Re-sort exact
|
|
3797
|
-
* by id — the same final tie-break _compareScoredRows applies.
|
|
3745
|
+
* The keyword index breaks equal-score ties by internal insertion order,
|
|
3746
|
+
* which syncEntries changes: a discarded-and-re-added id moves to the end.
|
|
3747
|
+
* Every caller truncates these results to a limit, so which ids survive a
|
|
3748
|
+
* tie at the boundary must not depend on write history. Re-sort exact
|
|
3749
|
+
* score ties by id — the same final tie-break _compareScoredRows applies.
|
|
3798
3750
|
*/
|
|
3799
3751
|
_compareSearchResults(a, b) {
|
|
3800
3752
|
const scoreDiff = b.score - a.score;
|
|
@@ -3815,6 +3767,198 @@ _SearchService.MAX_VECTOR_CACHE_ENTITIES = 16;
|
|
|
3815
3767
|
*/
|
|
3816
3768
|
_SearchService.MAX_VECTOR_CACHE_FACTS_PER_ENTITY = 500;
|
|
3817
3769
|
var SearchService = _SearchService;
|
|
3770
|
+
function finiteResults(results) {
|
|
3771
|
+
return results.every((r) => Number.isFinite(r.score)) ? results : results.filter((r) => Number.isFinite(r.score));
|
|
3772
|
+
}
|
|
3773
|
+
var MiniSearchIndexStrategy = class {
|
|
3774
|
+
constructor() {
|
|
3775
|
+
/**
|
|
3776
|
+
* Per-entity id set. `replaceEntity()` and `hasIndexedEntity()` read it;
|
|
3777
|
+
* `addDocuments()` and `discardIds()` keep it in sync. `SearchService` does
|
|
3778
|
+
* not see this map; it only sees `hasIndexedEntity`. The previous
|
|
3779
|
+
* `miniSearchEntryIdsByEntity` lived on `SearchService`.
|
|
3780
|
+
*/
|
|
3781
|
+
this.entryIdsByEntity = /* @__PURE__ */ new Map();
|
|
3782
|
+
/**
|
|
3783
|
+
* Reverse of `entryIdsByEntity`. `replace(entityId, ids, …)` uses it to
|
|
3784
|
+
* decide which ids are owned by `entityId` (the only safe membership test
|
|
3785
|
+
* — a rebuild that failed between its discard pass and the set replacement
|
|
3786
|
+
* can leave the forward set claiming ids the MiniSearch instance does not
|
|
3787
|
+
* hold). The pre-extraction `syncEntries` deleted from one set per id, not
|
|
3788
|
+
* from every entity's set.
|
|
3789
|
+
*/
|
|
3790
|
+
this.entityIdById = /* @__PURE__ */ new Map();
|
|
3791
|
+
this.index = this.createIndex();
|
|
3792
|
+
}
|
|
3793
|
+
/**
|
|
3794
|
+
* Build a fresh MiniSearch instance with the project's index options.
|
|
3795
|
+
* Called from the constructor and from `replaceAll`. We do not use
|
|
3796
|
+
* `removeAll()` on an existing instance because that empties the index but
|
|
3797
|
+
* leaves `dirtCount` and the vacuum bookkeeping intact, which would trip
|
|
3798
|
+
* `syncEntries`' conditional vacuum early after a clear.
|
|
3799
|
+
*/
|
|
3800
|
+
createIndex() {
|
|
3801
|
+
return new MiniSearch__default.default({
|
|
3802
|
+
fields: ["title", "body", "tags"],
|
|
3803
|
+
storeFields: ["entity_id"],
|
|
3804
|
+
// Vacuuming is driven explicitly at the end of each serialized rebuild
|
|
3805
|
+
// (see SearchService.syncEntries). Auto-vacuum fires on its own
|
|
3806
|
+
// schedule, asynchronously with respect to the caller, and traversing
|
|
3807
|
+
// the tree mid-rebuild is what threw the uncaught TypeError in
|
|
3808
|
+
// MiniSearch.performVacuuming (#64).
|
|
3809
|
+
autoVacuum: false,
|
|
3810
|
+
searchOptions: {
|
|
3811
|
+
boost: { title: 2 },
|
|
3812
|
+
fuzzy: 0.2,
|
|
3813
|
+
prefix: true
|
|
3814
|
+
}
|
|
3815
|
+
});
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* Incremental sync. Removes only ids currently tracked under `entityId`
|
|
3819
|
+
* (other entities' docs are untouched), then upserts `documents` by id.
|
|
3820
|
+
* The same id appearing in both `ids` and `documents` is discarded first
|
|
3821
|
+
* so the upsert does not throw on a duplicate id.
|
|
3822
|
+
*
|
|
3823
|
+
* Every document must carry `entityId` as its `entity_id`; a mismatch is
|
|
3824
|
+
* rejected before the write mutates anything. A tracking-only call
|
|
3825
|
+
* (`replace(entityId, [], [])`) registers the entity as indexed, matching
|
|
3826
|
+
* the interface contract.
|
|
3827
|
+
*
|
|
3828
|
+
* Atomicity: never awaits between the discard pass and `addDocuments`, so
|
|
3829
|
+
* a concurrent `search()` sees the index wholly before or wholly after,
|
|
3830
|
+
* never half-updated. This is the precondition the pre-extraction
|
|
3831
|
+
* `syncEntries` kept with its "no await from here to `addAll`" comment.
|
|
3832
|
+
*/
|
|
3833
|
+
async replace(entityId, ids, documents) {
|
|
3834
|
+
assertOwnedBy(entityId, documents);
|
|
3835
|
+
this.discardIds(ids, entityId);
|
|
3836
|
+
if (!this.entryIdsByEntity.has(entityId)) this.entryIdsByEntity.set(entityId, /* @__PURE__ */ new Set());
|
|
3837
|
+
this.addDocuments(documents);
|
|
3838
|
+
}
|
|
3839
|
+
/**
|
|
3840
|
+
* Per-entity rebuild. Removes every id tracked under `entityId` (with the
|
|
3841
|
+
* `index.has(id)` guard so a stale tracking set cannot make `discard()`
|
|
3842
|
+
* throw), then writes `documents`. The entity stays registered as
|
|
3843
|
+
* indexed — even when `documents` is empty — so the next `syncEntries`
|
|
3844
|
+
* call can take the fast path. Every document must belong to `entityId`.
|
|
3845
|
+
*/
|
|
3846
|
+
async replaceEntity(entityId, documents) {
|
|
3847
|
+
assertOwnedBy(entityId, documents);
|
|
3848
|
+
const previous = this.entryIdsByEntity.get(entityId);
|
|
3849
|
+
if (previous) this.discardIds([...previous]);
|
|
3850
|
+
this.entryIdsByEntity.set(entityId, /* @__PURE__ */ new Set());
|
|
3851
|
+
this.addDocuments(documents);
|
|
3852
|
+
}
|
|
3853
|
+
/**
|
|
3854
|
+
* Global rebuild / clear. Replaces the MiniSearch instance and clears both
|
|
3855
|
+
* id maps, so `hasIndexedEntity` returns `false` for every entity afterwards.
|
|
3856
|
+
* `SearchService.clearAll()` calls `replaceAll([])`.
|
|
3857
|
+
*/
|
|
3858
|
+
async replaceAll(documents) {
|
|
3859
|
+
this.index = this.createIndex();
|
|
3860
|
+
this.entryIdsByEntity.clear();
|
|
3861
|
+
this.entityIdById.clear();
|
|
3862
|
+
this.addDocuments(documents);
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Best-match-first search over the documents whose `entity_id` is in
|
|
3866
|
+
* `options.entityIds`. `preFilterLimit` overrides `limit` (hybrid retrieval
|
|
3867
|
+
* uses this to over-fetch candidates before the `limit` cap). Scores are
|
|
3868
|
+
* raw BM25+ — `SearchService.getKeywordScores` and `searchKeyword` apply
|
|
3869
|
+
* any scaling, not the strategy.
|
|
3870
|
+
*/
|
|
3871
|
+
async search(query, options) {
|
|
3872
|
+
if (options.entityIds.length === 0) return [];
|
|
3873
|
+
const entityIdSet = new Set(options.entityIds);
|
|
3874
|
+
const limit = options.preFilterLimit ?? options.limit;
|
|
3875
|
+
const raw = this.index.search(query, {
|
|
3876
|
+
filter: (r) => entityIdSet.has(r.entity_id),
|
|
3877
|
+
combineWith: "OR"
|
|
3878
|
+
});
|
|
3879
|
+
raw.sort((a, b) => {
|
|
3880
|
+
const scoreDiff = b.score - a.score;
|
|
3881
|
+
if (!Number.isNaN(scoreDiff) && scoreDiff !== 0) return scoreDiff;
|
|
3882
|
+
return a.id.localeCompare(b.id);
|
|
3883
|
+
});
|
|
3884
|
+
return raw.slice(0, limit).map((r) => ({
|
|
3885
|
+
id: r.id,
|
|
3886
|
+
entity_id: r.entity_id,
|
|
3887
|
+
score: r.score
|
|
3888
|
+
}));
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Explicit vacuum. MiniSearch 7.2.0's `dirtCount` rule: search after an
|
|
3892
|
+
* un-vacuumed discard inverts the relative scores of term-sharing
|
|
3893
|
+
* documents at any index size. `SearchService.syncEntries` calls this
|
|
3894
|
+
* after a discard-bearing turn. No-op when `dirtCount === 0`.
|
|
3895
|
+
*/
|
|
3896
|
+
async vacuum() {
|
|
3897
|
+
if (this.index.dirtCount > 0) {
|
|
3898
|
+
await this.index.vacuum();
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
/**
|
|
3902
|
+
* `true` when `entityId` has been indexed at least once under the current
|
|
3903
|
+
* strategy state. Backed by `entryIdsByEntity.has(entityId)`, which is
|
|
3904
|
+
* kept in sync by `addDocuments`, `replaceEntity`, and `replaceAll`.
|
|
3905
|
+
*/
|
|
3906
|
+
hasIndexedEntity(entityId) {
|
|
3907
|
+
return this.entryIdsByEntity.has(entityId);
|
|
3908
|
+
}
|
|
3909
|
+
/**
|
|
3910
|
+
* Add `documents` to the MiniSearch instance and register them in
|
|
3911
|
+
* `entryIdsByEntity` and `entityIdById`. MiniSearch mutates documents
|
|
3912
|
+
* during `add`, so each doc is copied first. `add` throws on an id the
|
|
3913
|
+
* index already holds — `replace` and `replaceEntity` discard first for
|
|
3914
|
+
* that reason.
|
|
3915
|
+
*
|
|
3916
|
+
* Adds and registers one document at a time: if a duplicate id throws
|
|
3917
|
+
* part-way through, every document already in the index is tracked, so
|
|
3918
|
+
* the next rebuild can discard it instead of failing on it again. This is
|
|
3919
|
+
* the precondition the per-turn recovery path relies on (#258 review).
|
|
3920
|
+
*/
|
|
3921
|
+
addDocuments(documents) {
|
|
3922
|
+
for (const d of documents) {
|
|
3923
|
+
const doc = { ...d };
|
|
3924
|
+
this.index.add(doc);
|
|
3925
|
+
const set = this.entryIdsByEntity.get(doc.entity_id) ?? /* @__PURE__ */ new Set();
|
|
3926
|
+
set.add(doc.id);
|
|
3927
|
+
this.entryIdsByEntity.set(doc.entity_id, set);
|
|
3928
|
+
this.entityIdById.set(doc.id, doc.entity_id);
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
/**
|
|
3932
|
+
* Discard `ids` from the MiniSearch instance and from both id maps, but
|
|
3933
|
+
* only for ids currently owned by `entityId` (the second argument). When
|
|
3934
|
+
* `entityId` is omitted (used by `replaceEntity` for the bulk rebuild
|
|
3935
|
+
* path), every id is discarded regardless of owning entity.
|
|
3936
|
+
*
|
|
3937
|
+
* `index.has(id)` is the membership test of record: a rebuild that failed
|
|
3938
|
+
* between its own discard pass and the set replacement can leave the
|
|
3939
|
+
* tracking set claiming ids the index no longer holds — `discard()` throws
|
|
3940
|
+
* on those, so we guard.
|
|
3941
|
+
*/
|
|
3942
|
+
discardIds(ids, entityId) {
|
|
3943
|
+
for (const id of ids) {
|
|
3944
|
+
const owning = this.entityIdById.get(id);
|
|
3945
|
+
if (owning === void 0) continue;
|
|
3946
|
+
if (entityId !== void 0 && owning !== entityId) continue;
|
|
3947
|
+
if (this.index.has(id)) this.index.discard(id);
|
|
3948
|
+
this.entryIdsByEntity.get(owning)?.delete(id);
|
|
3949
|
+
this.entityIdById.delete(id);
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
};
|
|
3953
|
+
function assertOwnedBy(entityId, documents) {
|
|
3954
|
+
for (const doc of documents) {
|
|
3955
|
+
if (doc.entity_id !== entityId) {
|
|
3956
|
+
throw new Error(
|
|
3957
|
+
`IndexStrategy write for entity ${entityId} got document ${doc.id} under entity ${doc.entity_id}`
|
|
3958
|
+
);
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3818
3962
|
|
|
3819
3963
|
// src/services/JobManager.ts
|
|
3820
3964
|
var JobManager = class {
|
|
@@ -6478,7 +6622,7 @@ async function gateCandidates(deps, ctx, candidates, cfg) {
|
|
|
6478
6622
|
}
|
|
6479
6623
|
}
|
|
6480
6624
|
if (!ranked) {
|
|
6481
|
-
ranked = deps.searchService.searchKeyword(`${c.fact.title} ${c.fact.body}`, [entityId], cfg.k + nonLive.size).filter((h) => !nonLive.has(h.id)).slice(0, cfg.k).map((h) => ({ id: h.id, score: null }));
|
|
6625
|
+
ranked = (await deps.searchService.searchKeyword(`${c.fact.title} ${c.fact.body}`, [entityId], cfg.k + nonLive.size)).filter((h) => !nonLive.has(h.id)).slice(0, cfg.k).map((h) => ({ id: h.id, score: null }));
|
|
6482
6626
|
}
|
|
6483
6627
|
const facts = ranked.length > 0 ? await deps.entryRepo.findByIds(ranked.map((r) => r.id), [entityId]) : [];
|
|
6484
6628
|
const byId = new Map(facts.map((f) => [f.id, f]));
|
|
@@ -7921,7 +8065,7 @@ var MaintenanceService = class {
|
|
|
7921
8065
|
if (!query) return [];
|
|
7922
8066
|
const cached = cache?.get(query);
|
|
7923
8067
|
if (cached) return cached;
|
|
7924
|
-
const hits = this.searchService.searchKeyword(
|
|
8068
|
+
const hits = await this.searchService.searchKeyword(
|
|
7925
8069
|
query,
|
|
7926
8070
|
[entityId],
|
|
7927
8071
|
cap * HEAL_ANCHOR_SEARCH_OVERFETCH
|
|
@@ -9093,7 +9237,7 @@ var RetrievalService = class {
|
|
|
9093
9237
|
let miniSearchScores;
|
|
9094
9238
|
if (effectivePreFilterLimit !== void 0) {
|
|
9095
9239
|
populateCache = false;
|
|
9096
|
-
const preResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER).filter((r) => !excludedIds.has(r.id));
|
|
9240
|
+
const preResults = (await this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, Number.MAX_SAFE_INTEGER)).filter((r) => !excludedIds.has(r.id));
|
|
9097
9241
|
if (preResults.length === 0) {
|
|
9098
9242
|
candidateRows = null;
|
|
9099
9243
|
} else {
|
|
@@ -9120,7 +9264,7 @@ var RetrievalService = class {
|
|
|
9120
9264
|
candidateRows = this._withoutIds(await this.entryRepo.findWithEmbeddingsByEntityIds(scoredEntityIds), excludedIds);
|
|
9121
9265
|
}
|
|
9122
9266
|
if (weight !== void 0 && weight < 1) {
|
|
9123
|
-
miniSearchScores = this.searchService.
|
|
9267
|
+
miniSearchScores = await this.searchService.getKeywordScores(trimmedQuery, scoredEntityIds);
|
|
9124
9268
|
}
|
|
9125
9269
|
}
|
|
9126
9270
|
if (candidateRows === null) {
|
|
@@ -9363,7 +9507,7 @@ var RetrievalService = class {
|
|
|
9363
9507
|
const hasActiveFloorsKeywordFallback = sanitizedTierFloors !== void 0 && Object.values(sanitizedTierFloors).some((f) => f > 0);
|
|
9364
9508
|
const keywordOversampledLimit = hasActiveFloorsKeywordFallback ? Number.MAX_SAFE_INTEGER : padLimit(Math.max(maxResults * 2, maxResults + 50));
|
|
9365
9509
|
const preFilteredIds = effectivePreFilterLimit !== void 0 ? new Set(candidateRows.map((r) => r.id)) : void 0;
|
|
9366
|
-
const keywordResults = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, keywordOversampledLimit);
|
|
9510
|
+
const keywordResults = await this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, keywordOversampledLimit);
|
|
9367
9511
|
const topResults = preFilteredIds === void 0 ? keywordResults : keywordResults.filter((r) => preFilteredIds.has(r.id));
|
|
9368
9512
|
const topResultIds = new Set(topResults.map((r) => r.id));
|
|
9369
9513
|
const candidateMap = new Map(candidateRows.filter((r) => topResultIds.has(r.id)).map((row) => [row.id, row]));
|
|
@@ -9466,7 +9610,7 @@ var RetrievalService = class {
|
|
|
9466
9610
|
if (!usedEmbed && scoredEntityIds.length > 0) {
|
|
9467
9611
|
const hasActiveFloors = sanitizedTierFloors !== void 0 && Object.values(sanitizedTierFloors).some((f) => f > 0);
|
|
9468
9612
|
const fallbackOversampledLimit = hasActiveFloors ? Number.MAX_SAFE_INTEGER : padLimit(Math.max(maxResults * 2, maxResults + 50));
|
|
9469
|
-
const results = this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, fallbackOversampledLimit).filter((r) => !excludedIds.has(r.id));
|
|
9613
|
+
const results = (await this.searchService.searchKeyword(trimmedQuery, scoredEntityIds, fallbackOversampledLimit)).filter((r) => !excludedIds.has(r.id));
|
|
9470
9614
|
const candidates = results.map((r) => ({
|
|
9471
9615
|
id: r.id,
|
|
9472
9616
|
entity_id: r.entity_id,
|
|
@@ -10298,7 +10442,10 @@ var WikiMemory = class {
|
|
|
10298
10442
|
);
|
|
10299
10443
|
this.lintService = new LintService(this.lintRepo, this.ontologyService);
|
|
10300
10444
|
this.embeddingService = new EmbeddingService(this.db, this.options, this.entryRepo, this.metadataRepo);
|
|
10301
|
-
this.searchService = new SearchService(
|
|
10445
|
+
this.searchService = new SearchService(
|
|
10446
|
+
this.entryRepo,
|
|
10447
|
+
new MiniSearchIndexStrategy()
|
|
10448
|
+
);
|
|
10302
10449
|
this.temporalService = new TemporalService(this.db, this.options, this.entryRepo, this.searchService, this.embeddingService);
|
|
10303
10450
|
this.jobManager = new JobManager(this.prefix);
|
|
10304
10451
|
this.promptService = new PromptService(options.config?.prompts, resolveGrounding(options.config?.grounding));
|
|
@@ -10691,6 +10838,32 @@ var WikiMemory = class {
|
|
|
10691
10838
|
clearVectorCache() {
|
|
10692
10839
|
this.searchService.evictCache();
|
|
10693
10840
|
}
|
|
10841
|
+
/**
|
|
10842
|
+
* Brings the keyword search index up to date with SQLite after a host
|
|
10843
|
+
* transaction that used `upsertGraph` has committed. Core never sees that
|
|
10844
|
+
* commit, so without this the nodes stay out of keyword, hybrid and
|
|
10845
|
+
* fallback retrieval until the entity's next core write.
|
|
10846
|
+
*
|
|
10847
|
+
* With `entityId`, rebuilds that entity if it is stale or has never been
|
|
10848
|
+
* indexed; otherwise it is a no-op that still waits for rebuilds already
|
|
10849
|
+
* queued. Without an argument, rebuilds every entity currently marked
|
|
10850
|
+
* stale; when none is, it resolves immediately and does not wait for rebuilds
|
|
10851
|
+
* already queued.
|
|
10852
|
+
*
|
|
10853
|
+
* Call only after the host's transaction commits: called inside it, the
|
|
10854
|
+
* rebuild may index rows that later roll back, or miss them and clear the
|
|
10855
|
+
* stale flag anyway. Serialized with core's internal index syncs and takes
|
|
10856
|
+
* no maintenance lock. Never rejects for a valid `entityId` — on failure the
|
|
10857
|
+
* entity stays stale and the next call retries it. Does not compute
|
|
10858
|
+
* embeddings; that remains the maintenance sweep's job.
|
|
10859
|
+
*
|
|
10860
|
+
* @throws {TypeError} when `entityId` is given but is not a non-empty string.
|
|
10861
|
+
*/
|
|
10862
|
+
async syncSearchIndex(entityId) {
|
|
10863
|
+
if (entityId === void 0) return this.searchService.syncStale();
|
|
10864
|
+
assertEntityId(entityId);
|
|
10865
|
+
return this.searchService.syncEntries(entityId, []);
|
|
10866
|
+
}
|
|
10694
10867
|
async exportDump(entityIds) {
|
|
10695
10868
|
return this.importExportService.exportDump(entityIds);
|
|
10696
10869
|
}
|
|
@@ -10737,9 +10910,11 @@ var WikiMemory = class {
|
|
|
10737
10910
|
* MUST be called from inside an open `db.withTransactionAsync` callback —
|
|
10738
10911
|
* the third argument is the caller's `tx`. The method does not open a
|
|
10739
10912
|
* nested transaction, does not acquire any lock, and does not perform
|
|
10740
|
-
* post-commit work (search sync, embedding, cache eviction).
|
|
10741
|
-
*
|
|
10742
|
-
* (`
|
|
10913
|
+
* post-commit work (search sync, embedding, cache eviction). After the
|
|
10914
|
+
* transaction commits, call `syncSearchIndex(entityId)` — or
|
|
10915
|
+
* `syncSearchIndex()` after writing several entities in one transaction —
|
|
10916
|
+
* to make the new nodes keyword-searchable. Embeddings still come from the maintenance
|
|
10917
|
+
* sweep (`runLibrarian` / `runHeal` / `runOntologyBackfill` / `runPrune`,
|
|
10743
10918
|
* scoped via `listEntityIds`).
|
|
10744
10919
|
*
|
|
10745
10920
|
* Contract (full pre-flight validation lives inside
|