@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.
- package/dist/{chunk-JV2OQ5CM.mjs → chunk-46X3HO77.mjs} +142 -14
- package/dist/chunk-46X3HO77.mjs.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +173 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +36 -6
- package/dist/index.mjs.map +1 -1
- package/dist/{testing-CH_Kk34-.d.mts → testing-BIFNLUva.d.mts} +69 -0
- package/dist/{testing-CH_Kk34-.d.ts → testing-BIFNLUva.d.ts} +69 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +140 -12
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/dist/chunk-JV2OQ5CM.mjs.map +0 -1
|
@@ -1171,6 +1171,14 @@ type ReembedCandidateRow = WikiFact & {
|
|
|
1171
1171
|
};
|
|
1172
1172
|
declare class EntryRepository extends BaseRepository {
|
|
1173
1173
|
private outbox;
|
|
1174
|
+
/**
|
|
1175
|
+
* Column list and liveness predicate shared by findMiniSearchRows and
|
|
1176
|
+
* findMiniSearchRowsByIds, so the full rebuild and the incremental read
|
|
1177
|
+
* can never drift apart (spec 2026-09-28 §4.4: both must produce
|
|
1178
|
+
* identical documents).
|
|
1179
|
+
*/
|
|
1180
|
+
private static readonly MINI_SEARCH_COLUMNS;
|
|
1181
|
+
private static readonly MINI_SEARCH_LIVE_WHERE;
|
|
1174
1182
|
private chunkSize;
|
|
1175
1183
|
constructor(db: SQLiteAdapter, prefix: string, outbox: OutboxRepository);
|
|
1176
1184
|
/**
|
|
@@ -1375,6 +1383,19 @@ declare class EntryRepository extends BaseRepository {
|
|
|
1375
1383
|
body: string;
|
|
1376
1384
|
tags: string;
|
|
1377
1385
|
}>>;
|
|
1386
|
+
/**
|
|
1387
|
+
* Keyword-index rows for specific ids — the incremental counterpart of
|
|
1388
|
+
* {@link findMiniSearchRows}, with identical columns. Soft-deleted rows and
|
|
1389
|
+
* rows of other entities are omitted, so a caller can pass every id a write
|
|
1390
|
+
* touched and get back exactly what should be indexed. Spec 2026-09-28 §4.4.
|
|
1391
|
+
*/
|
|
1392
|
+
findMiniSearchRowsByIds(entityId: string, ids: readonly string[], tx?: SQLiteAdapter): Promise<Array<{
|
|
1393
|
+
id: string;
|
|
1394
|
+
entity_id: string;
|
|
1395
|
+
title: string;
|
|
1396
|
+
body: string;
|
|
1397
|
+
tags: string;
|
|
1398
|
+
}>>;
|
|
1378
1399
|
updateEmbeddingBlob(id: string, blob: Uint8Array, tx?: SQLiteAdapter): Promise<void>;
|
|
1379
1400
|
/**
|
|
1380
1401
|
* Record a failed embedding attempt. Deliberately does NOT touch updated_at
|
|
@@ -1702,7 +1723,29 @@ declare class SearchService {
|
|
|
1702
1723
|
* auto-vacuum debt behind the TypeError in #64.
|
|
1703
1724
|
*/
|
|
1704
1725
|
private syncChain;
|
|
1726
|
+
/**
|
|
1727
|
+
* Entities whose index may have drifted from SQLite: an incremental update
|
|
1728
|
+
* failed part-way, or core wrote rows it could not index (upsertGraph runs in
|
|
1729
|
+
* the host's transaction). Their next syncEntries rebuilds the entity in full.
|
|
1730
|
+
* See spec 2026-09-28 §5.
|
|
1731
|
+
*/
|
|
1732
|
+
private staleEntities;
|
|
1733
|
+
/**
|
|
1734
|
+
* Per-entity count of markStale() calls. A rebuild clears an entity's stale
|
|
1735
|
+
* flag only when the count it snapshotted before its read still holds
|
|
1736
|
+
* afterwards: markStale() runs inside the host's still-open transaction, so
|
|
1737
|
+
* a call landing mid-read belongs to rows the read cannot have seen, and
|
|
1738
|
+
* its flag must outlive the turn (#233 review).
|
|
1739
|
+
*/
|
|
1740
|
+
private staleEpochs;
|
|
1705
1741
|
constructor(entryRepo: EntryRepository);
|
|
1742
|
+
/**
|
|
1743
|
+
* A fresh index with the production options. clearAll() swaps one in because
|
|
1744
|
+
* MiniSearch.removeAll() empties the index but leaves dirtCount (and its
|
|
1745
|
+
* vacuum bookkeeping) at its old value, which would trip syncEntries'
|
|
1746
|
+
* conditional vacuum early after a clear.
|
|
1747
|
+
*/
|
|
1748
|
+
private createMiniSearch;
|
|
1706
1749
|
/**
|
|
1707
1750
|
* Rebuilds the search index and clears the vector cache for a given entity.
|
|
1708
1751
|
* A direct replacement for manually syncing state after a DB transaction.
|
|
@@ -1712,6 +1755,24 @@ declare class SearchService {
|
|
|
1712
1755
|
* correct failure mode and killing the host process is not.
|
|
1713
1756
|
*/
|
|
1714
1757
|
sync(entityId?: string): Promise<void>;
|
|
1758
|
+
/**
|
|
1759
|
+
* Re-indexes only `ids` for `entityId`: drops each from the index, then
|
|
1760
|
+
* re-adds the ones still live in SQLite, so soft-deleted or missing ids end
|
|
1761
|
+
* up absent. Costs O(ids), where sync(entityId) costs O(entity) — callers that
|
|
1762
|
+
* know what a write touched use this so chunked imports stay linear (#232).
|
|
1763
|
+
*
|
|
1764
|
+
* Serialized with sync() on the same chain and, like it, never rejects. An
|
|
1765
|
+
* entity that is stale or has never been indexed gets a full rebuild instead
|
|
1766
|
+
* — including on an empty `ids` list, which is a no-op only for an entity
|
|
1767
|
+
* the index already tracks, and even then one that waits for rebuilds
|
|
1768
|
+
* already queued on the chain.
|
|
1769
|
+
*/
|
|
1770
|
+
syncEntries(entityId: string, ids: Iterable<string>): Promise<void>;
|
|
1771
|
+
/**
|
|
1772
|
+
* Forces the entity's next syncEntries to rebuild it in full. For writes core
|
|
1773
|
+
* cannot index itself, such as upsertGraph inside a host transaction.
|
|
1774
|
+
*/
|
|
1775
|
+
markStale(entityId: string): void;
|
|
1715
1776
|
/**
|
|
1716
1777
|
* Clears the parsed vector cache. Useful for mid-loop flush guarantees
|
|
1717
1778
|
* or memory pressure evictions.
|
|
@@ -1738,6 +1799,14 @@ declare class SearchService {
|
|
|
1738
1799
|
private normalizeMiniSearchRow;
|
|
1739
1800
|
private _tieBreakSort;
|
|
1740
1801
|
private _compareScoredRows;
|
|
1802
|
+
/**
|
|
1803
|
+
* MiniSearch breaks equal-score ties by internal insertion order, which
|
|
1804
|
+
* syncEntries changes: a discarded-and-re-added id moves to the end. Every
|
|
1805
|
+
* caller truncates these results to a limit, so which ids survive a tie at
|
|
1806
|
+
* the boundary must not depend on write history. Re-sort exact score ties
|
|
1807
|
+
* by id — the same final tie-break _compareScoredRows applies.
|
|
1808
|
+
*/
|
|
1809
|
+
private _compareSearchResults;
|
|
1741
1810
|
}
|
|
1742
1811
|
|
|
1743
1812
|
type OperationType = 'prune' | 'librarian' | 'heal' | 'ingest' | 'reembed' | 'global_reembed' | 'import' | 'global_import' | 'forget' | 'ontologyBackfill';
|
|
@@ -1171,6 +1171,14 @@ type ReembedCandidateRow = WikiFact & {
|
|
|
1171
1171
|
};
|
|
1172
1172
|
declare class EntryRepository extends BaseRepository {
|
|
1173
1173
|
private outbox;
|
|
1174
|
+
/**
|
|
1175
|
+
* Column list and liveness predicate shared by findMiniSearchRows and
|
|
1176
|
+
* findMiniSearchRowsByIds, so the full rebuild and the incremental read
|
|
1177
|
+
* can never drift apart (spec 2026-09-28 §4.4: both must produce
|
|
1178
|
+
* identical documents).
|
|
1179
|
+
*/
|
|
1180
|
+
private static readonly MINI_SEARCH_COLUMNS;
|
|
1181
|
+
private static readonly MINI_SEARCH_LIVE_WHERE;
|
|
1174
1182
|
private chunkSize;
|
|
1175
1183
|
constructor(db: SQLiteAdapter, prefix: string, outbox: OutboxRepository);
|
|
1176
1184
|
/**
|
|
@@ -1375,6 +1383,19 @@ declare class EntryRepository extends BaseRepository {
|
|
|
1375
1383
|
body: string;
|
|
1376
1384
|
tags: string;
|
|
1377
1385
|
}>>;
|
|
1386
|
+
/**
|
|
1387
|
+
* Keyword-index rows for specific ids — the incremental counterpart of
|
|
1388
|
+
* {@link findMiniSearchRows}, with identical columns. Soft-deleted rows and
|
|
1389
|
+
* rows of other entities are omitted, so a caller can pass every id a write
|
|
1390
|
+
* touched and get back exactly what should be indexed. Spec 2026-09-28 §4.4.
|
|
1391
|
+
*/
|
|
1392
|
+
findMiniSearchRowsByIds(entityId: string, ids: readonly string[], tx?: SQLiteAdapter): Promise<Array<{
|
|
1393
|
+
id: string;
|
|
1394
|
+
entity_id: string;
|
|
1395
|
+
title: string;
|
|
1396
|
+
body: string;
|
|
1397
|
+
tags: string;
|
|
1398
|
+
}>>;
|
|
1378
1399
|
updateEmbeddingBlob(id: string, blob: Uint8Array, tx?: SQLiteAdapter): Promise<void>;
|
|
1379
1400
|
/**
|
|
1380
1401
|
* Record a failed embedding attempt. Deliberately does NOT touch updated_at
|
|
@@ -1702,7 +1723,29 @@ declare class SearchService {
|
|
|
1702
1723
|
* auto-vacuum debt behind the TypeError in #64.
|
|
1703
1724
|
*/
|
|
1704
1725
|
private syncChain;
|
|
1726
|
+
/**
|
|
1727
|
+
* Entities whose index may have drifted from SQLite: an incremental update
|
|
1728
|
+
* failed part-way, or core wrote rows it could not index (upsertGraph runs in
|
|
1729
|
+
* the host's transaction). Their next syncEntries rebuilds the entity in full.
|
|
1730
|
+
* See spec 2026-09-28 §5.
|
|
1731
|
+
*/
|
|
1732
|
+
private staleEntities;
|
|
1733
|
+
/**
|
|
1734
|
+
* Per-entity count of markStale() calls. A rebuild clears an entity's stale
|
|
1735
|
+
* flag only when the count it snapshotted before its read still holds
|
|
1736
|
+
* afterwards: markStale() runs inside the host's still-open transaction, so
|
|
1737
|
+
* a call landing mid-read belongs to rows the read cannot have seen, and
|
|
1738
|
+
* its flag must outlive the turn (#233 review).
|
|
1739
|
+
*/
|
|
1740
|
+
private staleEpochs;
|
|
1705
1741
|
constructor(entryRepo: EntryRepository);
|
|
1742
|
+
/**
|
|
1743
|
+
* A fresh index with the production options. clearAll() swaps one in because
|
|
1744
|
+
* MiniSearch.removeAll() empties the index but leaves dirtCount (and its
|
|
1745
|
+
* vacuum bookkeeping) at its old value, which would trip syncEntries'
|
|
1746
|
+
* conditional vacuum early after a clear.
|
|
1747
|
+
*/
|
|
1748
|
+
private createMiniSearch;
|
|
1706
1749
|
/**
|
|
1707
1750
|
* Rebuilds the search index and clears the vector cache for a given entity.
|
|
1708
1751
|
* A direct replacement for manually syncing state after a DB transaction.
|
|
@@ -1712,6 +1755,24 @@ declare class SearchService {
|
|
|
1712
1755
|
* correct failure mode and killing the host process is not.
|
|
1713
1756
|
*/
|
|
1714
1757
|
sync(entityId?: string): Promise<void>;
|
|
1758
|
+
/**
|
|
1759
|
+
* Re-indexes only `ids` for `entityId`: drops each from the index, then
|
|
1760
|
+
* re-adds the ones still live in SQLite, so soft-deleted or missing ids end
|
|
1761
|
+
* up absent. Costs O(ids), where sync(entityId) costs O(entity) — callers that
|
|
1762
|
+
* know what a write touched use this so chunked imports stay linear (#232).
|
|
1763
|
+
*
|
|
1764
|
+
* Serialized with sync() on the same chain and, like it, never rejects. An
|
|
1765
|
+
* entity that is stale or has never been indexed gets a full rebuild instead
|
|
1766
|
+
* — including on an empty `ids` list, which is a no-op only for an entity
|
|
1767
|
+
* the index already tracks, and even then one that waits for rebuilds
|
|
1768
|
+
* already queued on the chain.
|
|
1769
|
+
*/
|
|
1770
|
+
syncEntries(entityId: string, ids: Iterable<string>): Promise<void>;
|
|
1771
|
+
/**
|
|
1772
|
+
* Forces the entity's next syncEntries to rebuild it in full. For writes core
|
|
1773
|
+
* cannot index itself, such as upsertGraph inside a host transaction.
|
|
1774
|
+
*/
|
|
1775
|
+
markStale(entityId: string): void;
|
|
1715
1776
|
/**
|
|
1716
1777
|
* Clears the parsed vector cache. Useful for mid-loop flush guarantees
|
|
1717
1778
|
* or memory pressure evictions.
|
|
@@ -1738,6 +1799,14 @@ declare class SearchService {
|
|
|
1738
1799
|
private normalizeMiniSearchRow;
|
|
1739
1800
|
private _tieBreakSort;
|
|
1740
1801
|
private _compareScoredRows;
|
|
1802
|
+
/**
|
|
1803
|
+
* MiniSearch breaks equal-score ties by internal insertion order, which
|
|
1804
|
+
* syncEntries changes: a discarded-and-re-added id moves to the end. Every
|
|
1805
|
+
* caller truncates these results to a limit, so which ids survive a tie at
|
|
1806
|
+
* the boundary must not depend on write history. Re-sort exact score ties
|
|
1807
|
+
* by id — the same final tie-break _compareScoredRows applies.
|
|
1808
|
+
*/
|
|
1809
|
+
private _compareSearchResults;
|
|
1741
1810
|
}
|
|
1742
1811
|
|
|
1743
1812
|
type OperationType = 'prune' | 'librarian' | 'heal' | 'ingest' | 'reembed' | 'global_reembed' | 'import' | 'global_import' | 'forget' | 'ontologyBackfill';
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { aq as EmbeddingService, ar as ImportExportService, as as IngestionService, at as JobManager, at as JobManagerType, au as MaintenanceService, av as RetrievalService, aw as SearchService, aw as SearchServiceType, ai as WikiMemoryTestAccess, ax as WriteService } from './testing-
|
|
1
|
+
export { aq as EmbeddingService, ar as ImportExportService, as as IngestionService, at as JobManager, at as JobManagerType, au as MaintenanceService, av as RetrievalService, aw as SearchService, aw as SearchServiceType, ai as WikiMemoryTestAccess, ax as WriteService } from './testing-BIFNLUva.mjs';
|
|
2
2
|
import '@equationalapplications/core-okf';
|
|
3
3
|
import 'minisearch';
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { aq as EmbeddingService, ar as ImportExportService, as as IngestionService, at as JobManager, at as JobManagerType, au as MaintenanceService, av as RetrievalService, aw as SearchService, aw as SearchServiceType, ai as WikiMemoryTestAccess, ax as WriteService } from './testing-
|
|
1
|
+
export { aq as EmbeddingService, ar as ImportExportService, as as IngestionService, at as JobManager, at as JobManagerType, au as MaintenanceService, av as RetrievalService, aw as SearchService, aw as SearchServiceType, ai as WikiMemoryTestAccess, ax as WriteService } from './testing-BIFNLUva.js';
|
|
2
2
|
import '@equationalapplications/core-okf';
|
|
3
3
|
import 'minisearch';
|
package/dist/testing.js
CHANGED
|
@@ -1343,7 +1343,11 @@ var ImportExportService = class {
|
|
|
1343
1343
|
);
|
|
1344
1344
|
}
|
|
1345
1345
|
});
|
|
1346
|
-
|
|
1346
|
+
if (merge) {
|
|
1347
|
+
await this.searchService.syncEntries(entityId, [...upsertedFactIds]);
|
|
1348
|
+
} else {
|
|
1349
|
+
await this.searchService.sync(entityId);
|
|
1350
|
+
}
|
|
1347
1351
|
for (const fact of bundle.facts) {
|
|
1348
1352
|
if (!fact.deleted_at && upsertedFactIds.has(fact.id) && !factsWithPreservedBlob.has(fact.id)) {
|
|
1349
1353
|
const clipped = clippedTextByFactId.get(fact.id);
|
|
@@ -2095,7 +2099,10 @@ var IngestionService = class {
|
|
|
2095
2099
|
return zeroChunkResult(canonical);
|
|
2096
2100
|
}
|
|
2097
2101
|
diagBuffer.flush(this.options);
|
|
2098
|
-
await this.searchService.
|
|
2102
|
+
await this.searchService.syncEntries(
|
|
2103
|
+
entityId,
|
|
2104
|
+
[...deletedSourceFactIds, ...insertedFacts.map((fact) => fact.id)]
|
|
2105
|
+
);
|
|
2099
2106
|
if (failedChunks === 0) {
|
|
2100
2107
|
const uniqueDeletedSourceFactIds = Array.from(new Set(deletedSourceFactIds));
|
|
2101
2108
|
for (const factId of uniqueDeletedSourceFactIds) {
|
|
@@ -2765,6 +2772,7 @@ var MaintenanceService = class {
|
|
|
2765
2772
|
this._validatePruneDuration(retainSoftDeletedFor, "retainSoftDeletedFor");
|
|
2766
2773
|
this._validatePruneDuration(retainEventsFor, "retainEventsFor");
|
|
2767
2774
|
const now = Date.now();
|
|
2775
|
+
let syncedIds = [];
|
|
2768
2776
|
let deletedEntries = 0;
|
|
2769
2777
|
let deletedTasks = 0;
|
|
2770
2778
|
let deletedEvents = 0;
|
|
@@ -2783,6 +2791,7 @@ var MaintenanceService = class {
|
|
|
2783
2791
|
}
|
|
2784
2792
|
}
|
|
2785
2793
|
const succeededIds = succeeded.map((r) => r.id);
|
|
2794
|
+
syncedIds = succeededIds;
|
|
2786
2795
|
await this.db.withTransactionAsync(async (tx) => {
|
|
2787
2796
|
if (succeededIds.length > 0) {
|
|
2788
2797
|
deletedEntries = await this.entryRepo.bulkDeletePruned(entityId, cutoff, succeededIds, tx);
|
|
@@ -2790,7 +2799,7 @@ var MaintenanceService = class {
|
|
|
2790
2799
|
deletedTasks = await this.taskRepo.bulkDeletePruned(entityId, cutoff, tx);
|
|
2791
2800
|
});
|
|
2792
2801
|
if (failure) {
|
|
2793
|
-
await this.searchService.
|
|
2802
|
+
await this.searchService.syncEntries(entityId, syncedIds);
|
|
2794
2803
|
const remaining = entriesToDelete.length - succeeded.length - 1;
|
|
2795
2804
|
const isTimeout = failure.cause?.[HOOK_TIMEOUT_MARKER] === true;
|
|
2796
2805
|
if (isTimeout) {
|
|
@@ -2824,7 +2833,7 @@ var MaintenanceService = class {
|
|
|
2824
2833
|
if (vacuum) {
|
|
2825
2834
|
await this.metadataRepo.vacuum();
|
|
2826
2835
|
}
|
|
2827
|
-
await this.searchService.
|
|
2836
|
+
await this.searchService.syncEntries(entityId, syncedIds);
|
|
2828
2837
|
return { entries: deletedEntries, tasks: deletedTasks, events: deletedEvents };
|
|
2829
2838
|
} finally {
|
|
2830
2839
|
this.jobManager.releaseLock("prune", entityId);
|
|
@@ -2994,8 +3003,8 @@ var MaintenanceService = class {
|
|
|
2994
3003
|
deletedEntries += refResult;
|
|
2995
3004
|
}
|
|
2996
3005
|
});
|
|
2997
|
-
await this.searchService.sync(entityId);
|
|
2998
3006
|
const uniqueDeletedIds = Array.from(new Set(deletedEntryIds));
|
|
3007
|
+
await this.searchService.syncEntries(entityId, uniqueDeletedIds);
|
|
2999
3008
|
for (const factId of uniqueDeletedIds) {
|
|
3000
3009
|
try {
|
|
3001
3010
|
await this.embeddingService.notifyEmbeddingPersistedOrThrow(entityId, factId, null);
|
|
@@ -3193,7 +3202,7 @@ var MaintenanceService = class {
|
|
|
3193
3202
|
}
|
|
3194
3203
|
});
|
|
3195
3204
|
diagBuffer.flush(this.options);
|
|
3196
|
-
await this.searchService.
|
|
3205
|
+
await this.searchService.syncEntries(entityId, insertedFacts.map((f) => f.id));
|
|
3197
3206
|
for (const fact of insertedFacts) {
|
|
3198
3207
|
await this.embeddingService.embedFact(fact, { operation: "librarian", trigger });
|
|
3199
3208
|
}
|
|
@@ -3251,7 +3260,7 @@ var MaintenanceService = class {
|
|
|
3251
3260
|
}
|
|
3252
3261
|
const healCandidates = await this.entryRepo.findHealCandidatesByEntityId(entityId, batchSize, recheckCutoff);
|
|
3253
3262
|
if (healCandidates.length === 0) {
|
|
3254
|
-
await this.searchService.
|
|
3263
|
+
await this.searchService.syncEntries(entityId, Array.from(/* @__PURE__ */ new Set([...orphanedIds, ...staleDowngradedIds])));
|
|
3255
3264
|
this.searchService.evictCache(entityId);
|
|
3256
3265
|
const counts2 = await this.entryRepo.countHealCandidatesByEntityId(entityId, recheckCutoff);
|
|
3257
3266
|
return {
|
|
@@ -3421,7 +3430,14 @@ var MaintenanceService = class {
|
|
|
3421
3430
|
diagBuffer.push({ ...diagBase, code: "heal_skipped", detail: { factId: item.id, reason } });
|
|
3422
3431
|
}
|
|
3423
3432
|
diagBuffer.flush(this.options);
|
|
3424
|
-
|
|
3433
|
+
const healSyncIds = Array.from(/* @__PURE__ */ new Set([
|
|
3434
|
+
...orphanedIds,
|
|
3435
|
+
...staleDowngradedIds,
|
|
3436
|
+
...safeDowngraded,
|
|
3437
|
+
...uniqueDeletedFactIds,
|
|
3438
|
+
...insertedFacts.map((f) => f.id)
|
|
3439
|
+
]));
|
|
3440
|
+
await this.searchService.syncEntries(entityId, healSyncIds);
|
|
3425
3441
|
for (const factId of uniqueDeletedFactIds) {
|
|
3426
3442
|
try {
|
|
3427
3443
|
await this.embeddingService.notifyEmbeddingPersisted(entityId, factId, null);
|
|
@@ -4506,7 +4522,31 @@ var _SearchService = class _SearchService {
|
|
|
4506
4522
|
* auto-vacuum debt behind the TypeError in #64.
|
|
4507
4523
|
*/
|
|
4508
4524
|
this.syncChain = Promise.resolve();
|
|
4509
|
-
|
|
4525
|
+
/**
|
|
4526
|
+
* Entities whose index may have drifted from SQLite: an incremental update
|
|
4527
|
+
* failed part-way, or core wrote rows it could not index (upsertGraph runs in
|
|
4528
|
+
* the host's transaction). Their next syncEntries rebuilds the entity in full.
|
|
4529
|
+
* See spec 2026-09-28 §5.
|
|
4530
|
+
*/
|
|
4531
|
+
this.staleEntities = /* @__PURE__ */ new Set();
|
|
4532
|
+
/**
|
|
4533
|
+
* Per-entity count of markStale() calls. A rebuild clears an entity's stale
|
|
4534
|
+
* flag only when the count it snapshotted before its read still holds
|
|
4535
|
+
* afterwards: markStale() runs inside the host's still-open transaction, so
|
|
4536
|
+
* a call landing mid-read belongs to rows the read cannot have seen, and
|
|
4537
|
+
* its flag must outlive the turn (#233 review).
|
|
4538
|
+
*/
|
|
4539
|
+
this.staleEpochs = /* @__PURE__ */ new Map();
|
|
4540
|
+
this.miniSearch = this.createMiniSearch();
|
|
4541
|
+
}
|
|
4542
|
+
/**
|
|
4543
|
+
* A fresh index with the production options. clearAll() swaps one in because
|
|
4544
|
+
* MiniSearch.removeAll() empties the index but leaves dirtCount (and its
|
|
4545
|
+
* vacuum bookkeeping) at its old value, which would trip syncEntries'
|
|
4546
|
+
* conditional vacuum early after a clear.
|
|
4547
|
+
*/
|
|
4548
|
+
createMiniSearch() {
|
|
4549
|
+
return new MiniSearch__default.default({
|
|
4510
4550
|
fields: ["title", "body", "tags"],
|
|
4511
4551
|
storeFields: ["entity_id"],
|
|
4512
4552
|
// Vacuuming is driven explicitly at the end of each serialized rebuild
|
|
@@ -4533,7 +4573,19 @@ var _SearchService = class _SearchService {
|
|
|
4533
4573
|
const work = this.syncChain.then(async () => {
|
|
4534
4574
|
try {
|
|
4535
4575
|
try {
|
|
4576
|
+
const epochsBefore = new Map(this.staleEpochs);
|
|
4536
4577
|
await this.rebuildIndex(entityId);
|
|
4578
|
+
if (entityId) {
|
|
4579
|
+
if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
|
|
4580
|
+
this.staleEntities.delete(entityId);
|
|
4581
|
+
}
|
|
4582
|
+
} else {
|
|
4583
|
+
for (const id of [...this.staleEntities]) {
|
|
4584
|
+
if ((this.staleEpochs.get(id) ?? 0) === (epochsBefore.get(id) ?? 0)) {
|
|
4585
|
+
this.staleEntities.delete(id);
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4537
4589
|
await this.miniSearch.vacuum();
|
|
4538
4590
|
} finally {
|
|
4539
4591
|
this.evictCache(entityId);
|
|
@@ -4545,6 +4597,68 @@ var _SearchService = class _SearchService {
|
|
|
4545
4597
|
this.syncChain = work;
|
|
4546
4598
|
return work;
|
|
4547
4599
|
}
|
|
4600
|
+
/**
|
|
4601
|
+
* Re-indexes only `ids` for `entityId`: drops each from the index, then
|
|
4602
|
+
* re-adds the ones still live in SQLite, so soft-deleted or missing ids end
|
|
4603
|
+
* up absent. Costs O(ids), where sync(entityId) costs O(entity) — callers that
|
|
4604
|
+
* know what a write touched use this so chunked imports stay linear (#232).
|
|
4605
|
+
*
|
|
4606
|
+
* Serialized with sync() on the same chain and, like it, never rejects. An
|
|
4607
|
+
* entity that is stale or has never been indexed gets a full rebuild instead
|
|
4608
|
+
* — including on an empty `ids` list, which is a no-op only for an entity
|
|
4609
|
+
* the index already tracks, and even then one that waits for rebuilds
|
|
4610
|
+
* already queued on the chain.
|
|
4611
|
+
*/
|
|
4612
|
+
async syncEntries(entityId, ids) {
|
|
4613
|
+
const uniqueIds = [...new Set(ids)];
|
|
4614
|
+
if (uniqueIds.length === 0 && !this.staleEntities.has(entityId) && this.miniSearchEntryIdsByEntity.has(entityId)) {
|
|
4615
|
+
return this.syncChain;
|
|
4616
|
+
}
|
|
4617
|
+
const work = this.syncChain.then(async () => {
|
|
4618
|
+
try {
|
|
4619
|
+
try {
|
|
4620
|
+
const epochsBefore = new Map(this.staleEpochs);
|
|
4621
|
+
const needsRebuild = () => this.staleEntities.has(entityId) || !this.miniSearchEntryIdsByEntity.has(entityId);
|
|
4622
|
+
if (!needsRebuild()) {
|
|
4623
|
+
const rows = await this.entryRepo.findMiniSearchRowsByIds(entityId, uniqueIds);
|
|
4624
|
+
const tracked = this.miniSearchEntryIdsByEntity.get(entityId);
|
|
4625
|
+
if (tracked && !this.staleEntities.has(entityId)) {
|
|
4626
|
+
for (const id of uniqueIds) {
|
|
4627
|
+
if (tracked.delete(id) && this.miniSearch.has(id)) this.miniSearch.discard(id);
|
|
4628
|
+
}
|
|
4629
|
+
const documents = rows.map((row) => this.normalizeMiniSearchRow(row));
|
|
4630
|
+
if (documents.length > 0) this.miniSearch.addAll(documents);
|
|
4631
|
+
for (const document of documents) tracked.add(document.id);
|
|
4632
|
+
if (this.miniSearch.dirtCount > 0) {
|
|
4633
|
+
await this.miniSearch.vacuum();
|
|
4634
|
+
}
|
|
4635
|
+
return;
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
await this.rebuildIndex(entityId);
|
|
4639
|
+
if ((this.staleEpochs.get(entityId) ?? 0) === (epochsBefore.get(entityId) ?? 0)) {
|
|
4640
|
+
this.staleEntities.delete(entityId);
|
|
4641
|
+
}
|
|
4642
|
+
await this.miniSearch.vacuum();
|
|
4643
|
+
} finally {
|
|
4644
|
+
this.evictCache(entityId);
|
|
4645
|
+
}
|
|
4646
|
+
} catch (err) {
|
|
4647
|
+
this.staleEntities.add(entityId);
|
|
4648
|
+
console.warn(`[WikiMemory] search index incremental sync failed for ${entityId}:`, err);
|
|
4649
|
+
}
|
|
4650
|
+
});
|
|
4651
|
+
this.syncChain = work;
|
|
4652
|
+
return work;
|
|
4653
|
+
}
|
|
4654
|
+
/**
|
|
4655
|
+
* Forces the entity's next syncEntries to rebuild it in full. For writes core
|
|
4656
|
+
* cannot index itself, such as upsertGraph inside a host transaction.
|
|
4657
|
+
*/
|
|
4658
|
+
markStale(entityId) {
|
|
4659
|
+
this.staleEntities.add(entityId);
|
|
4660
|
+
this.staleEpochs.set(entityId, (this.staleEpochs.get(entityId) ?? 0) + 1);
|
|
4661
|
+
}
|
|
4548
4662
|
/**
|
|
4549
4663
|
* Clears the parsed vector cache. Useful for mid-loop flush guarantees
|
|
4550
4664
|
* or memory pressure evictions.
|
|
@@ -4561,8 +4675,10 @@ var _SearchService = class _SearchService {
|
|
|
4561
4675
|
*/
|
|
4562
4676
|
clearAll() {
|
|
4563
4677
|
this.vectorCache.clear();
|
|
4564
|
-
this.miniSearch.
|
|
4678
|
+
this.miniSearch = this.createMiniSearch();
|
|
4565
4679
|
this.miniSearchEntryIdsByEntity.clear();
|
|
4680
|
+
this.staleEntities.clear();
|
|
4681
|
+
this.staleEpochs.clear();
|
|
4566
4682
|
}
|
|
4567
4683
|
/**
|
|
4568
4684
|
* Executes a keyword search against the active MiniSearch index.
|
|
@@ -4573,7 +4689,7 @@ var _SearchService = class _SearchService {
|
|
|
4573
4689
|
filter: (r) => entityIdSet.has(r.entity_id),
|
|
4574
4690
|
combineWith: "OR"
|
|
4575
4691
|
});
|
|
4576
|
-
return results.slice(0, limit);
|
|
4692
|
+
return results.sort((a, b) => this._compareSearchResults(a, b)).slice(0, limit);
|
|
4577
4693
|
}
|
|
4578
4694
|
/**
|
|
4579
4695
|
* Pre-fetches MiniSearch scores for candidate hydration, used during hybrid weighting.
|
|
@@ -4583,7 +4699,7 @@ var _SearchService = class _SearchService {
|
|
|
4583
4699
|
let results = this.miniSearch.search(query, {
|
|
4584
4700
|
filter: (r) => entityIdSet.has(r.entity_id),
|
|
4585
4701
|
combineWith: "OR"
|
|
4586
|
-
});
|
|
4702
|
+
}).sort((a, b) => this._compareSearchResults(a, b));
|
|
4587
4703
|
if (preFilterLimit !== void 0) {
|
|
4588
4704
|
results = results.slice(0, preFilterLimit);
|
|
4589
4705
|
}
|
|
@@ -4711,6 +4827,18 @@ var _SearchService = class _SearchService {
|
|
|
4711
4827
|
if (updatedAtDiff !== 0) return updatedAtDiff;
|
|
4712
4828
|
return a.id.localeCompare(b.id);
|
|
4713
4829
|
}
|
|
4830
|
+
/**
|
|
4831
|
+
* MiniSearch breaks equal-score ties by internal insertion order, which
|
|
4832
|
+
* syncEntries changes: a discarded-and-re-added id moves to the end. Every
|
|
4833
|
+
* caller truncates these results to a limit, so which ids survive a tie at
|
|
4834
|
+
* the boundary must not depend on write history. Re-sort exact score ties
|
|
4835
|
+
* by id — the same final tie-break _compareScoredRows applies.
|
|
4836
|
+
*/
|
|
4837
|
+
_compareSearchResults(a, b) {
|
|
4838
|
+
const scoreDiff = b.score - a.score;
|
|
4839
|
+
if (!Number.isNaN(scoreDiff) && scoreDiff !== 0) return scoreDiff;
|
|
4840
|
+
return a.id.localeCompare(b.id);
|
|
4841
|
+
}
|
|
4714
4842
|
};
|
|
4715
4843
|
/**
|
|
4716
4844
|
* Maximum number of entities whose parsed embedding vectors are held in
|