@remnic/core 10.0.51 → 10.0.53
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/access-admin-ops-surface.js +24 -24
- package/dist/access-authorization-probe.js +25 -25
- package/dist/access-boundary.js +24 -24
- package/dist/access-cli.js +65 -65
- package/dist/access-extraction-force-flush.js +24 -24
- package/dist/access-http-briefing.js +25 -25
- package/dist/access-http-query.js +26 -26
- package/dist/access-http.js +40 -40
- package/dist/access-identity-continuity-surface.js +24 -24
- package/dist/access-lcm-surface.js +24 -24
- package/dist/access-mcp.js +33 -33
- package/dist/access-namespace-preflight.js +24 -24
- package/dist/access-observe-write-surface.js +24 -24
- package/dist/access-operations-batch.js +25 -25
- package/dist/access-operations.js +32 -32
- package/dist/access-recall-concurrency.js +24 -24
- package/dist/access-recall-response.js +24 -24
- package/dist/access-recall-surface.js +24 -24
- package/dist/access-service.js +24 -24
- package/dist/access-surface-catalog.js +26 -26
- package/dist/active-recall.js +9 -9
- package/dist/briefing.js +10 -10
- package/dist/buffer.js +5 -5
- package/dist/causal-consolidation.js +11 -11
- package/dist/{chunk-3NL4ALUP.js → chunk-24SIB3EW.js} +3 -3
- package/dist/{chunk-Y3T5KQCR.js → chunk-2EYCW2ET.js} +2 -2
- package/dist/{chunk-AHGOOZUC.js → chunk-34UVJNSG.js} +2 -2
- package/dist/{chunk-2EXF6IIB.js → chunk-3632XJMR.js} +2 -2
- package/dist/{chunk-KETS5A62.js → chunk-3H3PNQEF.js} +2 -2
- package/dist/{chunk-SFRBUBUZ.js → chunk-3LNAQLW4.js} +3 -3
- package/dist/{chunk-75BK4XGV.js → chunk-4BUJICMI.js} +10 -10
- package/dist/{chunk-KIWO4327.js → chunk-4W5B5Z6P.js} +2 -2
- package/dist/{chunk-B35EQQ7L.js → chunk-53OFDW3P.js} +3 -3
- package/dist/{chunk-2ONKXEC7.js → chunk-5LBBXI2A.js} +2 -2
- package/dist/{chunk-UCLJTGP3.js → chunk-65B5RFKQ.js} +2 -2
- package/dist/{chunk-KAJYSO3U.js → chunk-6CDNOREO.js} +2 -2
- package/dist/{chunk-POJRFSJM.js → chunk-7XGNGLAN.js} +2 -2
- package/dist/{chunk-A6JDT6QF.js → chunk-AO5ZOD32.js} +2 -2
- package/dist/{chunk-RA7YRL6H.js → chunk-AOSCGNIC.js} +3 -3
- package/dist/{chunk-LWMLBVGB.js → chunk-B43P3DTC.js} +2 -2
- package/dist/{chunk-SJD334AG.js → chunk-CA6X37OB.js} +217 -173
- package/dist/chunk-CA6X37OB.js.map +1 -0
- package/dist/{chunk-VUMWVYMR.js → chunk-CIMD2MUR.js} +3 -3
- package/dist/{chunk-XRFLR3XF.js → chunk-DLZGSZVC.js} +22 -9
- package/dist/chunk-DLZGSZVC.js.map +1 -0
- package/dist/{chunk-5CWH2LKL.js → chunk-DOJBV6WR.js} +29 -14
- package/dist/chunk-DOJBV6WR.js.map +1 -0
- package/dist/{chunk-44EUCSG4.js → chunk-FFQNNFHX.js} +2 -2
- package/dist/{chunk-HEF4BRMT.js → chunk-FZLZUDJJ.js} +2 -2
- package/dist/{chunk-L3EFYRYK.js → chunk-GGFQAGOT.js} +7 -7
- package/dist/{chunk-ZLCBLDJO.js → chunk-GIRWNTMV.js} +2 -2
- package/dist/{chunk-EC2KVA27.js → chunk-H4DWSOBQ.js} +169 -105
- package/dist/chunk-H4DWSOBQ.js.map +1 -0
- package/dist/{chunk-SHNM7RIT.js → chunk-HJ4X4BGS.js} +2 -2
- package/dist/{chunk-MUNYUSBU.js → chunk-HJQ4NKKO.js} +2 -2
- package/dist/{chunk-V6Z4NAJZ.js → chunk-HM3ZTDAX.js} +2 -2
- package/dist/{chunk-EZS6QJNC.js → chunk-IFOCK3OJ.js} +2 -2
- package/dist/{chunk-LXXIV7Y2.js → chunk-JEX2JL3U.js} +7 -2
- package/dist/{chunk-LXXIV7Y2.js.map → chunk-JEX2JL3U.js.map} +1 -1
- package/dist/{chunk-DF6ZF2BN.js → chunk-LIVNX7WK.js} +2 -2
- package/dist/{chunk-WY66J4PI.js → chunk-LRVFS4KA.js} +2 -2
- package/dist/{chunk-LMB47UM6.js → chunk-LTDK6HBV.js} +2 -2
- package/dist/{chunk-HCIRR3BA.js → chunk-M4U25HLR.js} +2 -2
- package/dist/{chunk-YK7ZJPQM.js → chunk-MGN2RQRG.js} +2 -2
- package/dist/{chunk-G26YNEOC.js → chunk-NWA6IXR3.js} +1 -1
- package/dist/{chunk-G26YNEOC.js.map → chunk-NWA6IXR3.js.map} +1 -1
- package/dist/{chunk-JIJAWTRK.js → chunk-OCXDZJL4.js} +264 -194
- package/dist/chunk-OCXDZJL4.js.map +1 -0
- package/dist/{chunk-IUQXRRJB.js → chunk-OMWQO7H6.js} +2 -2
- package/dist/{chunk-7VO6E5UD.js → chunk-OVZIYLBZ.js} +6 -2
- package/dist/chunk-OVZIYLBZ.js.map +1 -0
- package/dist/{chunk-SPTPBKZX.js → chunk-OWNAWY6D.js} +2 -2
- package/dist/{chunk-XKDQTDXN.js → chunk-PFUXPTBT.js} +36 -15
- package/dist/chunk-PFUXPTBT.js.map +1 -0
- package/dist/{chunk-DQOGZZJA.js → chunk-QAHP5SBG.js} +3 -3
- package/dist/{chunk-GBGU5OZP.js → chunk-QF5EJ5VP.js} +13 -13
- package/dist/{chunk-FT75AQ74.js → chunk-QS5DUSBM.js} +2 -2
- package/dist/{chunk-72CBJ5VP.js → chunk-R3WZ74B2.js} +3 -3
- package/dist/{chunk-PDTNQLPC.js → chunk-RG3I2663.js} +2 -2
- package/dist/{chunk-CK6GCGMX.js → chunk-RTJJPW3C.js} +2 -2
- package/dist/{chunk-RJDYDF5K.js → chunk-TRAOZ2WP.js} +2 -2
- package/dist/{chunk-6ZAU7LAR.js → chunk-TTTPTQ6S.js} +2 -2
- package/dist/{chunk-JS44PBCM.js → chunk-U2A56LLF.js} +6 -6
- package/dist/{chunk-MC7PXJDP.js → chunk-UBOWDOL5.js} +5 -5
- package/dist/{chunk-4FM35YIA.js → chunk-UQYBPPVC.js} +2 -2
- package/dist/{chunk-OCRLJJOV.js → chunk-V4VCBFAL.js} +3 -3
- package/dist/{chunk-FNNGWN3O.js → chunk-VGE4PUY6.js} +2 -2
- package/dist/{chunk-5DVOBBTV.js → chunk-VV73XT3A.js} +2 -2
- package/dist/{chunk-GE3ZX6YK.js → chunk-W7NKMRAF.js} +2 -2
- package/dist/{chunk-AJJPXUE6.js → chunk-WBN66HPI.js} +2 -2
- package/dist/{chunk-XWJUFER3.js → chunk-WGFEGBFB.js} +2 -2
- package/dist/{chunk-43TUWQNR.js → chunk-XDKEHZUC.js} +2 -2
- package/dist/{chunk-ROAKLYOL.js → chunk-XHEKS4UN.js} +2 -2
- package/dist/{chunk-ZKK3VMWU.js → chunk-XVTB6BZS.js} +2 -2
- package/dist/{chunk-AZLD4CKG.js → chunk-Y56D5FSD.js} +17 -17
- package/dist/{chunk-MPWOAIIH.js → chunk-YDT3OEYF.js} +2 -2
- package/dist/{chunk-IN6U2BS5.js → chunk-YJHOVA42.js} +2 -2
- package/dist/{chunk-TC55M5PO.js → chunk-YLN4ZQV4.js} +2 -2
- package/dist/cli.js +50 -50
- package/dist/coding/export-okf-codegraph.js +9 -9
- package/dist/compounding/engine.js +11 -11
- package/dist/config.js +9 -9
- package/dist/connectors/codex-materialize-runner.js +10 -10
- package/dist/connectors/index.js +10 -10
- package/dist/convergence-refresh.js +12 -12
- package/dist/corpus-watermark.js +12 -12
- package/dist/entity-retrieval.js +10 -10
- package/dist/external-wiki-access.js +25 -25
- package/dist/extraction.js +14 -14
- package/dist/harmonic-retrieval.js +10 -10
- package/dist/index.js +65 -65
- package/dist/location/index.js +12 -12
- package/dist/maintenance/memory-governance.js +10 -10
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -10
- package/dist/maintenance/rebuild-memory-projection-runner.js +12 -12
- package/dist/maintenance/rebuild-memory-projection.js +11 -11
- package/dist/memory-browse-operations.js +25 -25
- package/dist/memory-browse.js +5 -5
- package/dist/namespaces/migrate.js +11 -11
- package/dist/namespaces/search.d.ts.map +1 -1
- package/dist/namespaces/search.js +8 -8
- package/dist/namespaces/storage.js +10 -10
- package/dist/operator-doctor-corpus.js +13 -13
- package/dist/operator-toolkit.js +18 -18
- package/dist/orchestration/extraction-persist-deps.d.ts +2 -0
- package/dist/orchestration/extraction-persist-deps.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist-promotion.d.ts +40 -10
- package/dist/orchestration/extraction-persist-promotion.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestration/lifecycle-pending-fallback-drain.d.ts +18 -0
- package/dist/orchestration/lifecycle-pending-fallback-drain.d.ts.map +1 -0
- package/dist/orchestration/maintenance.d.ts +11 -13
- package/dist/orchestration/maintenance.d.ts.map +1 -1
- package/dist/orchestration/maintenance.js +20 -20
- package/dist/orchestration/qmd-maintenance-completion.d.ts +20 -0
- package/dist/orchestration/qmd-maintenance-completion.d.ts.map +1 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +65 -65
- package/dist/qmd.js +2 -2
- package/dist/recall-composition-decision.js +2 -2
- package/dist/recall-navigation-operations.js +25 -25
- package/dist/recall-navigation.js +24 -24
- package/dist/recall-state-view-admission.js +5 -5
- package/dist/recall-why-service.js +5 -5
- package/dist/resume-bundles.js +9 -9
- package/dist/search/factory.js +7 -7
- package/dist/search/index.js +7 -7
- package/dist/search/lancedb-backend.d.ts +3 -0
- package/dist/search/lancedb-backend.d.ts.map +1 -1
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.d.ts +3 -0
- package/dist/search/meilisearch-backend.d.ts.map +1 -1
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.d.ts +3 -0
- package/dist/search/orama-backend.d.ts.map +1 -1
- package/dist/search/orama-backend.js +2 -2
- package/dist/search/port.d.ts +7 -0
- package/dist/search/port.d.ts.map +1 -1
- package/dist/search/port.js +1 -1
- package/dist/search/remote-backend.d.ts +2 -0
- package/dist/search/remote-backend.d.ts.map +1 -1
- package/dist/search/remote-backend.js +2 -2
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +10 -10
- package/dist/semantic-rule-verifier.js +10 -10
- package/dist/source-agent-qualifier.js +13 -13
- package/dist/standup/build.js +10 -10
- package/dist/storage/entity-canonical-id-migration-adapter.d.ts +7 -1
- package/dist/storage/entity-canonical-id-migration-adapter.d.ts.map +1 -1
- package/dist/storage/entity-migration-maintenance-pending.d.ts +31 -0
- package/dist/storage/entity-migration-maintenance-pending.d.ts.map +1 -0
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +9 -9
- package/dist/support-passport/index.js +27 -27
- package/dist/transfer/export-okf.js +10 -10
- package/dist/verified-recall.js +10 -10
- package/dist/who-knows-cli.js +11 -11
- package/dist/who-knows.js +10 -10
- package/package.json +2 -2
- package/src/namespaces/search.test.ts +121 -0
- package/src/namespaces/search.ts +8 -0
- package/src/orchestration/extraction-persist-deps.ts +3 -0
- package/src/orchestration/extraction-persist-promotion.ts +104 -16
- package/src/orchestration/extraction-persist.ts +66 -77
- package/src/orchestration/lifecycle-pending-fallback-drain.ts +97 -0
- package/src/orchestration/maintenance.test.ts +443 -1
- package/src/orchestration/maintenance.ts +73 -95
- package/src/orchestration/qmd-maintenance-completion.ts +32 -0
- package/src/orchestrator-entity-write-failure.test.ts +370 -0
- package/src/orchestrator.ts +9 -1
- package/src/search/lancedb-backend.ts +29 -12
- package/src/search/meilisearch-backend.test.ts +148 -0
- package/src/search/meilisearch-backend.ts +41 -14
- package/src/search/orama-backend.test.ts +75 -1
- package/src/search/orama-backend.ts +22 -7
- package/src/search/port.ts +7 -0
- package/src/search/remote-backend.ts +4 -0
- package/src/storage/entity-canonical-id-migration-adapter.ts +44 -9
- package/src/storage/entity-migration-maintenance-pending-helpers.test.ts +87 -0
- package/src/storage/entity-migration-maintenance-pending.test.ts +242 -0
- package/src/storage/entity-migration-maintenance-pending.ts +89 -0
- package/src/storage.ts +2 -2
- package/dist/chunk-5CWH2LKL.js.map +0 -1
- package/dist/chunk-7VO6E5UD.js.map +0 -1
- package/dist/chunk-EC2KVA27.js.map +0 -1
- package/dist/chunk-JIJAWTRK.js.map +0 -1
- package/dist/chunk-SJD334AG.js.map +0 -1
- package/dist/chunk-XKDQTDXN.js.map +0 -1
- package/dist/chunk-XRFLR3XF.js.map +0 -1
- /package/dist/{chunk-3NL4ALUP.js.map → chunk-24SIB3EW.js.map} +0 -0
- /package/dist/{chunk-Y3T5KQCR.js.map → chunk-2EYCW2ET.js.map} +0 -0
- /package/dist/{chunk-AHGOOZUC.js.map → chunk-34UVJNSG.js.map} +0 -0
- /package/dist/{chunk-2EXF6IIB.js.map → chunk-3632XJMR.js.map} +0 -0
- /package/dist/{chunk-KETS5A62.js.map → chunk-3H3PNQEF.js.map} +0 -0
- /package/dist/{chunk-SFRBUBUZ.js.map → chunk-3LNAQLW4.js.map} +0 -0
- /package/dist/{chunk-75BK4XGV.js.map → chunk-4BUJICMI.js.map} +0 -0
- /package/dist/{chunk-KIWO4327.js.map → chunk-4W5B5Z6P.js.map} +0 -0
- /package/dist/{chunk-B35EQQ7L.js.map → chunk-53OFDW3P.js.map} +0 -0
- /package/dist/{chunk-2ONKXEC7.js.map → chunk-5LBBXI2A.js.map} +0 -0
- /package/dist/{chunk-UCLJTGP3.js.map → chunk-65B5RFKQ.js.map} +0 -0
- /package/dist/{chunk-KAJYSO3U.js.map → chunk-6CDNOREO.js.map} +0 -0
- /package/dist/{chunk-POJRFSJM.js.map → chunk-7XGNGLAN.js.map} +0 -0
- /package/dist/{chunk-A6JDT6QF.js.map → chunk-AO5ZOD32.js.map} +0 -0
- /package/dist/{chunk-RA7YRL6H.js.map → chunk-AOSCGNIC.js.map} +0 -0
- /package/dist/{chunk-LWMLBVGB.js.map → chunk-B43P3DTC.js.map} +0 -0
- /package/dist/{chunk-VUMWVYMR.js.map → chunk-CIMD2MUR.js.map} +0 -0
- /package/dist/{chunk-44EUCSG4.js.map → chunk-FFQNNFHX.js.map} +0 -0
- /package/dist/{chunk-HEF4BRMT.js.map → chunk-FZLZUDJJ.js.map} +0 -0
- /package/dist/{chunk-L3EFYRYK.js.map → chunk-GGFQAGOT.js.map} +0 -0
- /package/dist/{chunk-ZLCBLDJO.js.map → chunk-GIRWNTMV.js.map} +0 -0
- /package/dist/{chunk-SHNM7RIT.js.map → chunk-HJ4X4BGS.js.map} +0 -0
- /package/dist/{chunk-MUNYUSBU.js.map → chunk-HJQ4NKKO.js.map} +0 -0
- /package/dist/{chunk-V6Z4NAJZ.js.map → chunk-HM3ZTDAX.js.map} +0 -0
- /package/dist/{chunk-EZS6QJNC.js.map → chunk-IFOCK3OJ.js.map} +0 -0
- /package/dist/{chunk-DF6ZF2BN.js.map → chunk-LIVNX7WK.js.map} +0 -0
- /package/dist/{chunk-WY66J4PI.js.map → chunk-LRVFS4KA.js.map} +0 -0
- /package/dist/{chunk-LMB47UM6.js.map → chunk-LTDK6HBV.js.map} +0 -0
- /package/dist/{chunk-HCIRR3BA.js.map → chunk-M4U25HLR.js.map} +0 -0
- /package/dist/{chunk-YK7ZJPQM.js.map → chunk-MGN2RQRG.js.map} +0 -0
- /package/dist/{chunk-IUQXRRJB.js.map → chunk-OMWQO7H6.js.map} +0 -0
- /package/dist/{chunk-SPTPBKZX.js.map → chunk-OWNAWY6D.js.map} +0 -0
- /package/dist/{chunk-DQOGZZJA.js.map → chunk-QAHP5SBG.js.map} +0 -0
- /package/dist/{chunk-GBGU5OZP.js.map → chunk-QF5EJ5VP.js.map} +0 -0
- /package/dist/{chunk-FT75AQ74.js.map → chunk-QS5DUSBM.js.map} +0 -0
- /package/dist/{chunk-72CBJ5VP.js.map → chunk-R3WZ74B2.js.map} +0 -0
- /package/dist/{chunk-PDTNQLPC.js.map → chunk-RG3I2663.js.map} +0 -0
- /package/dist/{chunk-CK6GCGMX.js.map → chunk-RTJJPW3C.js.map} +0 -0
- /package/dist/{chunk-RJDYDF5K.js.map → chunk-TRAOZ2WP.js.map} +0 -0
- /package/dist/{chunk-6ZAU7LAR.js.map → chunk-TTTPTQ6S.js.map} +0 -0
- /package/dist/{chunk-JS44PBCM.js.map → chunk-U2A56LLF.js.map} +0 -0
- /package/dist/{chunk-MC7PXJDP.js.map → chunk-UBOWDOL5.js.map} +0 -0
- /package/dist/{chunk-4FM35YIA.js.map → chunk-UQYBPPVC.js.map} +0 -0
- /package/dist/{chunk-OCRLJJOV.js.map → chunk-V4VCBFAL.js.map} +0 -0
- /package/dist/{chunk-FNNGWN3O.js.map → chunk-VGE4PUY6.js.map} +0 -0
- /package/dist/{chunk-5DVOBBTV.js.map → chunk-VV73XT3A.js.map} +0 -0
- /package/dist/{chunk-GE3ZX6YK.js.map → chunk-W7NKMRAF.js.map} +0 -0
- /package/dist/{chunk-AJJPXUE6.js.map → chunk-WBN66HPI.js.map} +0 -0
- /package/dist/{chunk-XWJUFER3.js.map → chunk-WGFEGBFB.js.map} +0 -0
- /package/dist/{chunk-43TUWQNR.js.map → chunk-XDKEHZUC.js.map} +0 -0
- /package/dist/{chunk-ROAKLYOL.js.map → chunk-XHEKS4UN.js.map} +0 -0
- /package/dist/{chunk-ZKK3VMWU.js.map → chunk-XVTB6BZS.js.map} +0 -0
- /package/dist/{chunk-AZLD4CKG.js.map → chunk-Y56D5FSD.js.map} +0 -0
- /package/dist/{chunk-MPWOAIIH.js.map → chunk-YDT3OEYF.js.map} +0 -0
- /package/dist/{chunk-IN6U2BS5.js.map → chunk-YJHOVA42.js.map} +0 -0
- /package/dist/{chunk-TC55M5PO.js.map → chunk-YLN4ZQV4.js.map} +0 -0
package/src/orchestrator.ts
CHANGED
|
@@ -865,6 +865,7 @@ export class Orchestrator {
|
|
|
865
865
|
invalidateGraphEdgeCache: (storage) => this.graphIndexFor(storage).invalidateEdgeCache(),
|
|
866
866
|
updateTemporalTagIndexes: (storage, persistedIds) =>
|
|
867
867
|
this.updateTemporalTagIndexes(storage, persistedIds),
|
|
868
|
+
requestQmdMaintenance: () => this.requestQmdMaintenance(),
|
|
868
869
|
});
|
|
869
870
|
}
|
|
870
871
|
return this._extractionPersistCoordinator;
|
|
@@ -1511,7 +1512,14 @@ export class Orchestrator {
|
|
|
1511
1512
|
onActivitySynced: (summary) => { if (!this.config.meetings.enabled) return; void this.getMeetingsService().then((s) => s.requestBuildForActivitySync(summary.ranAt, this.config.activity.timezone, summary.touchedDates)).catch((err) => log.warn(`meetings: activity tail-step build failed to schedule: ${err instanceof Error ? err.message : String(err)}`)); },
|
|
1512
1513
|
});
|
|
1513
1514
|
const bindStorage = (storage: StorageManager, namespace: string) => {
|
|
1514
|
-
storage.onEntityMigrationComplete = () => {
|
|
1515
|
+
storage.onEntityMigrationComplete = () => {
|
|
1516
|
+
if (!this.acceptingEntityMigrationMaintenance) return false;
|
|
1517
|
+
// Settles once a QMD pass covering this request ran; the migration keeps its pending marker until then.
|
|
1518
|
+
const served = this.maintenanceScheduler.requestQmdMaintenanceCompletion();
|
|
1519
|
+
// Same side effects as before: projection rebuild and ledger-compaction checks, independent of QMD.
|
|
1520
|
+
this.requestQmdMaintenance();
|
|
1521
|
+
return served;
|
|
1522
|
+
};
|
|
1515
1523
|
if (this.prioritizedEmbedding) storage.onMemoryWrite = (filePath) => this.prioritizedEmbedding?.enqueue(filePath, namespace);
|
|
1516
1524
|
};
|
|
1517
1525
|
bindStorage(this.storage, this.config.defaultNamespace);
|
|
@@ -139,23 +139,38 @@ export class LanceDbBackend implements SearchBackend {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
async updateCollectionFromDir(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<void> {
|
|
142
|
-
|
|
142
|
+
await this.refreshCollection(collection, memoryDir, execution);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async updateCompleted(execution?: SearchExecutionOptions): Promise<boolean> {
|
|
146
|
+
try {
|
|
147
|
+
return await this.refreshCollection(this.collection, this.memoryDir, execution);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
log.debug(`LanceDbBackend update did not complete: ${err}`);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Brings the collection up to date; true only when it ran to completion (false: aborted, unavailable, skipped or failed). */
|
|
155
|
+
private async refreshCollection(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<boolean> {
|
|
156
|
+
if (isSearchAborted(execution)) return false;
|
|
143
157
|
let table = await this.ensureTableForCollection(collection);
|
|
144
|
-
if (isSearchAborted(execution)) return;
|
|
145
|
-
if (!table) return;
|
|
158
|
+
if (isSearchAborted(execution)) return false;
|
|
159
|
+
if (!table) return false;
|
|
146
160
|
|
|
147
161
|
const docs = await scanMemoryDir(memoryDir);
|
|
148
|
-
if (isSearchAborted(execution)) return;
|
|
162
|
+
if (isSearchAborted(execution)) return false;
|
|
149
163
|
if (docs.length === 0) {
|
|
150
164
|
// Clear stale data when no docs remain
|
|
151
165
|
try {
|
|
152
166
|
const db = await this.ensureDb();
|
|
153
167
|
await db.dropTable(collection).catch(() => {});
|
|
154
168
|
if (collection === this.collection) this.table = null;
|
|
169
|
+
return true;
|
|
155
170
|
} catch {
|
|
156
171
|
// Best-effort cleanup
|
|
157
172
|
}
|
|
158
|
-
return;
|
|
173
|
+
return false;
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
const embeddingProviderIdentity = this.embedHelper.getProviderIdentity();
|
|
@@ -166,13 +181,13 @@ export class LanceDbBackend implements SearchBackend {
|
|
|
166
181
|
const vectorProviderColumnState = await this.tableVectorProviderColumnState(table);
|
|
167
182
|
if (vectorProviderColumnState === "missing") {
|
|
168
183
|
table = await this.recreateTableForCollection(collection);
|
|
169
|
-
if (isSearchAborted(execution)) return;
|
|
170
|
-
if (!table) return;
|
|
184
|
+
if (isSearchAborted(execution)) return false;
|
|
185
|
+
if (!table) return false;
|
|
171
186
|
} else if (vectorProviderColumnState === "present") {
|
|
172
187
|
try {
|
|
173
188
|
const existingRows = await table.query().select(["docid", "vector", "vectorProvider"]).toArray();
|
|
174
189
|
for (const row of existingRows ?? []) {
|
|
175
|
-
if (isSearchAborted(execution)) return;
|
|
190
|
+
if (isSearchAborted(execution)) return false;
|
|
176
191
|
const docid = row.docid;
|
|
177
192
|
if (typeof docid !== "string") continue;
|
|
178
193
|
const vector = row.vector;
|
|
@@ -184,11 +199,11 @@ export class LanceDbBackend implements SearchBackend {
|
|
|
184
199
|
}
|
|
185
200
|
} catch {
|
|
186
201
|
log.debug("LanceDbBackend skipped refresh after vector preservation failed");
|
|
187
|
-
return;
|
|
202
|
+
return false;
|
|
188
203
|
}
|
|
189
204
|
} else {
|
|
190
205
|
log.debug("LanceDbBackend skipped vector preservation after vectorProvider probe failed");
|
|
191
|
-
return;
|
|
206
|
+
return false;
|
|
192
207
|
}
|
|
193
208
|
|
|
194
209
|
const rows = docs.map((d) => {
|
|
@@ -213,19 +228,21 @@ export class LanceDbBackend implements SearchBackend {
|
|
|
213
228
|
});
|
|
214
229
|
|
|
215
230
|
try {
|
|
216
|
-
if (isSearchAborted(execution)) return;
|
|
231
|
+
if (isSearchAborted(execution)) return false;
|
|
217
232
|
await table.add(rows, { mode: "overwrite" });
|
|
218
233
|
this.rememberVectorProviderCompatibility(
|
|
219
234
|
table,
|
|
220
235
|
embeddingProviderIdentity,
|
|
221
236
|
rows.length > 0 && rows.every((row) => row.vectorProvider === embeddingProviderIdentity),
|
|
222
237
|
);
|
|
223
|
-
if (isSearchAborted(execution)) return;
|
|
238
|
+
if (isSearchAborted(execution)) return false;
|
|
224
239
|
// FTS index on the content column.
|
|
225
240
|
await this.createFtsIndex(table);
|
|
226
241
|
if (collection === this.collection) this.table = table;
|
|
242
|
+
return true;
|
|
227
243
|
} catch (err) {
|
|
228
244
|
log.debug(`LanceDbBackend update failed: ${err}`);
|
|
245
|
+
return false;
|
|
229
246
|
}
|
|
230
247
|
}
|
|
231
248
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { MeilisearchBackend } from "./meilisearch-backend.js";
|
|
8
|
+
|
|
9
|
+
// Issue #3286: update() is fail-open on every backend, so the entity-migration
|
|
10
|
+
// pending marker asks updateCompleted() whether the re-index actually ran.
|
|
11
|
+
test("Meilisearch updateCompleted is false whenever an enabled refresh returned early", async () => {
|
|
12
|
+
const base = { host: "http://127.0.0.1:1", collection: "openclaw-engram", memoryDir: "/nonexistent" };
|
|
13
|
+
assert.equal(await new MeilisearchBackend({ ...base, autoIndex: true }).updateCompleted(), false, "never probed: unavailable");
|
|
14
|
+
assert.equal(await new MeilisearchBackend({ host: base.host, collection: base.collection, autoIndex: true }).updateCompleted(), false, "no memory dir");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("Meilisearch updateCompleted is true when autoIndex is off: nothing is pushed by configuration (issue #3286)", async () => {
|
|
18
|
+
// autoIndex defaults to off (the server is managed outside Remnic). Reporting false would
|
|
19
|
+
// make every strict namespace update fail and leave the pending marker stuck.
|
|
20
|
+
const base = { host: "http://127.0.0.1:1", collection: "openclaw-engram", memoryDir: "/nonexistent" };
|
|
21
|
+
assert.equal(await new MeilisearchBackend({ ...base, autoIndex: false }).updateCompleted(), true);
|
|
22
|
+
assert.equal(await new MeilisearchBackend({ host: base.host, collection: base.collection }).updateCompleted(), true, "default config, no memory dir, never probed");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** A Meilisearch client double: waitForTask resolves with the task status, as the real 0.46 SDK does. */
|
|
26
|
+
function stubClient(status: string) {
|
|
27
|
+
return {
|
|
28
|
+
async health() {},
|
|
29
|
+
async getIndex() {},
|
|
30
|
+
index: () => ({
|
|
31
|
+
async addDocuments() {
|
|
32
|
+
return { taskUid: 1 };
|
|
33
|
+
},
|
|
34
|
+
async getDocuments() {
|
|
35
|
+
return { results: [] };
|
|
36
|
+
},
|
|
37
|
+
async deleteDocuments() {
|
|
38
|
+
return { taskUid: 2 };
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
// Resolves for failed and canceled tasks too; only a timeout rejects.
|
|
42
|
+
async waitForTask() {
|
|
43
|
+
return { status };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function reachableBackend(memoryDir: string, status: string): MeilisearchBackend {
|
|
49
|
+
const backend = new MeilisearchBackend({ host: "http://127.0.0.1:1", collection: "openclaw-engram", autoIndex: true, memoryDir });
|
|
50
|
+
Object.defineProperty(backend, "available", { value: true });
|
|
51
|
+
Object.defineProperty(backend, "client", { value: stubClient(status) });
|
|
52
|
+
return backend;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test("Meilisearch updateCompleted is true only when the add task succeeded (issue #3286)", async () => {
|
|
56
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-meili-completed-"));
|
|
57
|
+
try {
|
|
58
|
+
await mkdir(path.join(root, "facts"), { recursive: true });
|
|
59
|
+
await writeFile(path.join(root, "facts", "fact.md"), "---\nid: mem-meili\n---\nA fact to index.");
|
|
60
|
+
assert.equal(await reachableBackend(root, "succeeded").updateCompleted(), true);
|
|
61
|
+
for (const status of ["failed", "canceled", "enqueued", "processing"]) {
|
|
62
|
+
assert.equal(await reachableBackend(root, status).updateCompleted(), false, `a ${status} task did not index anything`);
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
await rm(root, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("Meilisearch updateCompleted is false for a reachable backend with no memory dir to index (issue #3286)", async () => {
|
|
70
|
+
const backend = new MeilisearchBackend({ host: "http://127.0.0.1:1", collection: "openclaw-engram", autoIndex: true });
|
|
71
|
+
Object.defineProperty(backend, "available", { value: true });
|
|
72
|
+
Object.defineProperty(backend, "client", { value: stubClient("succeeded") });
|
|
73
|
+
assert.equal(await backend.updateCompleted(), false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("Meilisearch updateCompleted is false when a stale-document deletion fails (issue #3286)", async () => {
|
|
77
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-meili-deletion-"));
|
|
78
|
+
try {
|
|
79
|
+
await mkdir(path.join(root, "facts"), { recursive: true });
|
|
80
|
+
// One live fact plus one stale server-side doc: the add succeeds, the deletion must too.
|
|
81
|
+
await writeFile(path.join(root, "facts", "fact.md"), "---\nid: mem-meili\n---\nA fact to index.");
|
|
82
|
+
const backend = new MeilisearchBackend({ host: "http://127.0.0.1:1", collection: "openclaw-engram", autoIndex: true, memoryDir: root });
|
|
83
|
+
Object.defineProperty(backend, "available", { value: true });
|
|
84
|
+
let waitForTaskCalls = 0;
|
|
85
|
+
Object.defineProperty(backend, "client", {
|
|
86
|
+
value: {
|
|
87
|
+
async health() {},
|
|
88
|
+
async getIndex() {},
|
|
89
|
+
index: () => ({
|
|
90
|
+
async addDocuments() {
|
|
91
|
+
return { taskUid: 1 };
|
|
92
|
+
},
|
|
93
|
+
async getDocuments() {
|
|
94
|
+
return { results: [{ id: "mem-meili" }, { id: "mem-stale" }] };
|
|
95
|
+
},
|
|
96
|
+
async deleteDocuments() {
|
|
97
|
+
return { taskUid: 2 };
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
// First wait is the add (succeeded); second is the stale-doc deletion.
|
|
101
|
+
async waitForTask() {
|
|
102
|
+
waitForTaskCalls += 1;
|
|
103
|
+
return { status: waitForTaskCalls === 1 ? "succeeded" : "failed" };
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
assert.equal(await backend.updateCompleted(), false, "a failed deletion leaves a stale document searchable");
|
|
108
|
+
} finally {
|
|
109
|
+
await rm(root, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("Meilisearch updateCompleted is false when waiting on the stale-document deletion throws (issue #3286)", async () => {
|
|
114
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-meili-deletion-throw-"));
|
|
115
|
+
try {
|
|
116
|
+
await mkdir(path.join(root, "facts"), { recursive: true });
|
|
117
|
+
await writeFile(path.join(root, "facts", "fact.md"), "---\nid: mem-meili\n---\nA fact to index.");
|
|
118
|
+
const backend = new MeilisearchBackend({ host: "http://127.0.0.1:1", collection: "openclaw-engram", autoIndex: true, memoryDir: root });
|
|
119
|
+
Object.defineProperty(backend, "available", { value: true });
|
|
120
|
+
let waitForTaskCalls = 0;
|
|
121
|
+
Object.defineProperty(backend, "client", {
|
|
122
|
+
value: {
|
|
123
|
+
async health() {},
|
|
124
|
+
async getIndex() {},
|
|
125
|
+
index: () => ({
|
|
126
|
+
async addDocuments() {
|
|
127
|
+
return { taskUid: 1 };
|
|
128
|
+
},
|
|
129
|
+
async getDocuments() {
|
|
130
|
+
return { results: [{ id: "mem-meili" }, { id: "mem-stale" }] };
|
|
131
|
+
},
|
|
132
|
+
async deleteDocuments() {
|
|
133
|
+
return { taskUid: 2 };
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
// The add succeeds; waiting on the deletion rejects (a timeout in the real SDK).
|
|
137
|
+
async waitForTask() {
|
|
138
|
+
waitForTaskCalls += 1;
|
|
139
|
+
if (waitForTaskCalls > 1) throw new Error("timed out waiting for the deletion task");
|
|
140
|
+
return { status: "succeeded" };
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
assert.equal(await backend.updateCompleted(), false);
|
|
145
|
+
} finally {
|
|
146
|
+
await rm(root, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
@@ -136,17 +136,30 @@ export class MeilisearchBackend implements SearchBackend {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
async updateCollectionFromDir(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<void> {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
await this.refreshCollection(collection, memoryDir, execution);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async updateCompleted(execution?: SearchExecutionOptions): Promise<boolean> {
|
|
143
|
+
// autoIndex is off by default: the Meilisearch server is managed outside Remnic and
|
|
144
|
+
// nothing is pushed by configuration, so there is no re-index that could be left incomplete.
|
|
145
|
+
if (!this.autoIndex) return true;
|
|
146
|
+
if (!this.memoryDir) return false;
|
|
147
|
+
return this.refreshCollection(this.collection, this.memoryDir, execution);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Brings the collection up to date; true only when it ran to completion (false: aborted, unavailable, skipped or failed). */
|
|
151
|
+
private async refreshCollection(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<boolean> {
|
|
152
|
+
if (!this.autoIndex) return false;
|
|
153
|
+
if (!this.available) return false;
|
|
154
|
+
if (isSearchAborted(execution)) return false;
|
|
142
155
|
|
|
143
156
|
try {
|
|
144
157
|
const ensured = await this.ensureCollection(memoryDir, collection, execution);
|
|
145
|
-
if (ensured === "skipped" || ensured === "missing") return;
|
|
158
|
+
if (ensured === "skipped" || ensured === "missing") return false;
|
|
146
159
|
const client = await this.ensureClient();
|
|
147
|
-
if (isSearchAborted(execution)) return;
|
|
160
|
+
if (isSearchAborted(execution)) return false;
|
|
148
161
|
const docs = await scanMemoryDir(memoryDir);
|
|
149
|
-
if (isSearchAborted(execution)) return;
|
|
162
|
+
if (isSearchAborted(execution)) return false;
|
|
150
163
|
const index = client.index(collection);
|
|
151
164
|
|
|
152
165
|
const meilDocs = docs.map((d) => ({
|
|
@@ -157,10 +170,15 @@ export class MeilisearchBackend implements SearchBackend {
|
|
|
157
170
|
}));
|
|
158
171
|
|
|
159
172
|
// Upsert current docs and wait for the task to complete
|
|
160
|
-
if (isSearchAborted(execution)) return;
|
|
173
|
+
if (isSearchAborted(execution)) return false;
|
|
161
174
|
const addTask = await index.addDocuments(meilDocs, { primaryKey: "id" });
|
|
162
|
-
|
|
163
|
-
|
|
175
|
+
// waitForTask resolves with a failed or canceled task and rejects only on timeout.
|
|
176
|
+
const added = await client.waitForTask(addTask.taskUid, { timeOutMs: this.timeoutMs });
|
|
177
|
+
if (added.status !== "succeeded") {
|
|
178
|
+
log.debug(`MeilisearchBackend add task finished with status ${added.status}`);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
if (isSearchAborted(execution)) return false;
|
|
164
182
|
|
|
165
183
|
// Remove docs that no longer exist on disk (paginated to handle large indexes)
|
|
166
184
|
const currentIds = new Set(docs.map((d) => d.docid));
|
|
@@ -170,7 +188,7 @@ export class MeilisearchBackend implements SearchBackend {
|
|
|
170
188
|
let staleIds: string[] = [];
|
|
171
189
|
let hasMore = true;
|
|
172
190
|
while (hasMore) {
|
|
173
|
-
if (isSearchAborted(execution)) return;
|
|
191
|
+
if (isSearchAborted(execution)) return false;
|
|
174
192
|
const page = await index.getDocuments({ limit: PAGE_SIZE, offset, fields: ["id"] });
|
|
175
193
|
const results = page.results ?? [];
|
|
176
194
|
for (const doc of results) {
|
|
@@ -181,15 +199,24 @@ export class MeilisearchBackend implements SearchBackend {
|
|
|
181
199
|
hasMore = results.length === PAGE_SIZE;
|
|
182
200
|
}
|
|
183
201
|
if (staleIds.length > 0) {
|
|
184
|
-
if (isSearchAborted(execution)) return;
|
|
202
|
+
if (isSearchAborted(execution)) return false;
|
|
185
203
|
const delTask = await index.deleteDocuments(staleIds);
|
|
186
|
-
|
|
204
|
+
// The deletion is part of completion: a stale document left searchable is not an
|
|
205
|
+
// up-to-date index, and waitForTask resolves failed/canceled tasks without throwing.
|
|
206
|
+
const deleted = await client.waitForTask(delTask.taskUid, { timeOutMs: this.timeoutMs });
|
|
207
|
+
if (deleted.status !== "succeeded") {
|
|
208
|
+
log.debug(`MeilisearchBackend stale-document deletion finished with status ${deleted.status}`);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
187
211
|
}
|
|
188
|
-
} catch {
|
|
189
|
-
|
|
212
|
+
} catch (err) {
|
|
213
|
+
log.debug(`MeilisearchBackend stale-document deletion failed: ${err}`);
|
|
214
|
+
return false;
|
|
190
215
|
}
|
|
216
|
+
return true;
|
|
191
217
|
} catch (err) {
|
|
192
218
|
log.debug(`MeilisearchBackend update failed: ${err}`);
|
|
219
|
+
return false;
|
|
193
220
|
}
|
|
194
221
|
}
|
|
195
222
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
@@ -232,3 +232,77 @@ test("Orama rebuild gate inspects every indexed string field, not only content (
|
|
|
232
232
|
await rm(root, { recursive: true, force: true });
|
|
233
233
|
}
|
|
234
234
|
});
|
|
235
|
+
|
|
236
|
+
test("Orama updateCompleted reports a finished refresh, and false for an aborted one (issue #3286)", async () => {
|
|
237
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-orama-completed-"));
|
|
238
|
+
try {
|
|
239
|
+
await writeMemoryFact(root, "fact.md", "---\nid: mem-completed\n---\nAn indexed fact about completion.");
|
|
240
|
+
const backend = await createBackend(root, path.join(root, "orama-db"), undefined, { update: false });
|
|
241
|
+
|
|
242
|
+
assert.equal(await backend.updateCompleted({ signal: AbortSignal.abort() }), false, "an aborted refresh did not finish");
|
|
243
|
+
assert.equal((await backend.bm25Search("completion")).length, 0, "precondition: nothing was indexed");
|
|
244
|
+
|
|
245
|
+
assert.equal(await backend.updateCompleted(), true);
|
|
246
|
+
assert.equal((await backend.bm25Search("completion")).some((r) => r.docid === "mem-completed"), true);
|
|
247
|
+
} finally {
|
|
248
|
+
await rm(root, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
/** A signal that reports aborted from its Nth read on: lets a test abort a refresh at each of its checkpoints. */
|
|
253
|
+
function abortingFromRead(read: number): AbortSignal {
|
|
254
|
+
const signal = new AbortController().signal;
|
|
255
|
+
let reads = 0;
|
|
256
|
+
Object.defineProperty(signal, "aborted", { get: () => ++reads >= read });
|
|
257
|
+
return signal;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
test("Orama updateCompleted is false for a refresh aborted at any checkpoint, and true only once it persisted (issue #3286)", async () => {
|
|
261
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-orama-abort-sweep-"));
|
|
262
|
+
try {
|
|
263
|
+
await writeMemoryFact(root, "kept.md", "---\nid: mem-kept\n---\nA fact that stays indexed.");
|
|
264
|
+
await writeMemoryFact(root, "second.md", "---\nid: mem-second\n---\nA second fact so the diff loops iterate.");
|
|
265
|
+
const dbPath = path.join(root, "orama-db");
|
|
266
|
+
// An existing, persisted index makes the refresh walk the stored hits as well as the corpus.
|
|
267
|
+
await createBackend(root, dbPath);
|
|
268
|
+
const persistedFile = resolveOramaCollectionDbFilePath(dbPath, "openclaw-engram");
|
|
269
|
+
const before = await readFile(persistedFile);
|
|
270
|
+
await rm(path.join(root, "facts", "second.md"));
|
|
271
|
+
|
|
272
|
+
// Every abort checkpoint must report false and leave the persisted index alone;
|
|
273
|
+
// the first run that reports true must be the one that persisted the change.
|
|
274
|
+
let completedAt = 0;
|
|
275
|
+
for (let read = 1; read < 200 && completedAt === 0; read += 1) {
|
|
276
|
+
// A fresh backend per attempt: an aborted refresh leaves its in-memory edits behind,
|
|
277
|
+
// which would shift the checkpoints of the next attempt.
|
|
278
|
+
const backend = await createBackend(root, dbPath, undefined, { update: false });
|
|
279
|
+
const completed = await backend.updateCompleted({ signal: abortingFromRead(read) });
|
|
280
|
+
const persisted = await readFile(persistedFile);
|
|
281
|
+
if (completed) {
|
|
282
|
+
completedAt = read;
|
|
283
|
+
assert.notDeepEqual(persisted, before, "a refresh reporting completion persisted the index");
|
|
284
|
+
} else {
|
|
285
|
+
assert.deepEqual(persisted, before, `aborted at read ${read}: nothing may be persisted`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
assert.ok(completedAt > 4, `the refresh checks for abort at several points (completed only at read ${completedAt})`);
|
|
289
|
+
} finally {
|
|
290
|
+
await rm(root, { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("Orama updateCompleted reports false, not a rejection, when the refresh throws (issue #3286)", async () => {
|
|
295
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-orama-throws-"));
|
|
296
|
+
try {
|
|
297
|
+
await writeMemoryFact(root, "fact.md", "---\nid: mem-throws\n---\nA fact.");
|
|
298
|
+
const backend = await createBackend(root, path.join(root, "orama-db"), undefined, { update: false });
|
|
299
|
+
Object.defineProperty(backend, "persistDbForCollection", {
|
|
300
|
+
value: async () => {
|
|
301
|
+
throw new Error("disk full");
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
assert.equal(await backend.updateCompleted(), false);
|
|
305
|
+
} finally {
|
|
306
|
+
await rm(root, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
@@ -178,14 +178,28 @@ export class OramaBackend implements SearchBackend {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
async updateCollectionFromDir(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<void> {
|
|
181
|
-
|
|
181
|
+
await this.refreshCollection(collection, memoryDir, execution);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async updateCompleted(execution?: SearchExecutionOptions): Promise<boolean> {
|
|
185
|
+
try {
|
|
186
|
+
return await this.refreshCollection(this.collection, this.memoryDir, execution);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
log.debug(`OramaBackend update did not complete: ${err}`);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Brings the collection up to date; true only when it ran to completion (false: aborted, unavailable, skipped or failed). */
|
|
194
|
+
private async refreshCollection(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<boolean> {
|
|
195
|
+
if (isSearchAborted(execution)) return false;
|
|
182
196
|
const db = await this.ensureDbForCollection(collection);
|
|
183
|
-
if (isSearchAborted(execution)) return;
|
|
184
|
-
if (!db) return;
|
|
197
|
+
if (isSearchAborted(execution)) return false;
|
|
198
|
+
if (!db) return false;
|
|
185
199
|
const { search: oramaSearch, insert, remove, count, getByID } = this.oramaModule;
|
|
186
200
|
|
|
187
201
|
const docs = await scanMemoryDir(memoryDir);
|
|
188
|
-
if (isSearchAborted(execution)) return;
|
|
202
|
+
if (isSearchAborted(execution)) return false;
|
|
189
203
|
const docMap = new Map(docs.map((d) => [d.docid, d]));
|
|
190
204
|
const { update: oramaUpdate } = this.oramaModule;
|
|
191
205
|
|
|
@@ -204,7 +218,7 @@ export class OramaBackend implements SearchBackend {
|
|
|
204
218
|
limit: existingCount + 100,
|
|
205
219
|
});
|
|
206
220
|
for (const hit of allHits.hits) {
|
|
207
|
-
if (isSearchAborted(execution)) return;
|
|
221
|
+
if (isSearchAborted(execution)) return false;
|
|
208
222
|
const storedDocument =
|
|
209
223
|
typeof getByID === "function"
|
|
210
224
|
? await getByID(db, hit.id)
|
|
@@ -227,7 +241,7 @@ export class OramaBackend implements SearchBackend {
|
|
|
227
241
|
|
|
228
242
|
// Insert new docs, update existing ones (preserving vectors since update is remove+insert)
|
|
229
243
|
for (const doc of docs) {
|
|
230
|
-
if (isSearchAborted(execution)) return;
|
|
244
|
+
if (isSearchAborted(execution)) return false;
|
|
231
245
|
const existing = existingDocs.get(doc.docid);
|
|
232
246
|
if (existing) {
|
|
233
247
|
const payload: Record<string, unknown> = {
|
|
@@ -281,13 +295,14 @@ export class OramaBackend implements SearchBackend {
|
|
|
281
295
|
}
|
|
282
296
|
}
|
|
283
297
|
|
|
284
|
-
if (isSearchAborted(execution)) return;
|
|
298
|
+
if (isSearchAborted(execution)) return false;
|
|
285
299
|
await this.persistDbForCollection(db, collection);
|
|
286
300
|
this.rememberVectorProviderCompatibility(
|
|
287
301
|
db,
|
|
288
302
|
embeddingProviderIdentity,
|
|
289
303
|
allRowsCompatible,
|
|
290
304
|
);
|
|
305
|
+
return true;
|
|
291
306
|
}
|
|
292
307
|
|
|
293
308
|
async embed(): Promise<void> {
|
package/src/search/port.ts
CHANGED
|
@@ -182,6 +182,13 @@ export interface SearchBackend {
|
|
|
182
182
|
* calls remain fail-open for migration/maintenance resilience.
|
|
183
183
|
*/
|
|
184
184
|
updateStrict?(execution?: SearchExecutionOptions): Promise<void>;
|
|
185
|
+
/**
|
|
186
|
+
* Fail-open like update(), but reports whether the index was actually brought
|
|
187
|
+
* up to date: false when it returned early (aborted, unavailable, suppressed
|
|
188
|
+
* or skipped) or failed. It never rejects for an update failure. For backends with no strict variant, so a caller
|
|
189
|
+
* that must not lose a re-index (the entity-migration pending marker) can tell.
|
|
190
|
+
*/
|
|
191
|
+
updateCompleted?(execution?: SearchExecutionOptions): Promise<boolean>;
|
|
185
192
|
updateCollection(collection: string, execution?: SearchExecutionOptions): Promise<void>;
|
|
186
193
|
updateCollectionFromDir?(collection: string, memoryDir: string, execution?: SearchExecutionOptions): Promise<void>;
|
|
187
194
|
/**
|
|
@@ -84,6 +84,10 @@ export class RemoteSearchBackend implements SearchBackend {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
async update(_execution?: SearchExecutionOptions): Promise<void> {}
|
|
87
|
+
/** The remote service owns its own indexing, so there is no local re-index that could be left incomplete. */
|
|
88
|
+
async updateCompleted(_execution?: SearchExecutionOptions): Promise<boolean> {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
87
91
|
async updateCollection(_collection: string, _execution?: SearchExecutionOptions): Promise<void> {}
|
|
88
92
|
async embed(): Promise<void> {}
|
|
89
93
|
async embedCollection(_collection: string): Promise<void> {}
|
|
@@ -10,6 +10,12 @@ import {
|
|
|
10
10
|
rewriteProjectedEntityReferences,
|
|
11
11
|
rewriteProjectedMemoryEntityReference,
|
|
12
12
|
} from "../memory-projection-mutations.js";
|
|
13
|
+
import {
|
|
14
|
+
listMaintenancePending,
|
|
15
|
+
settleMaintenancePending,
|
|
16
|
+
trackMaintenanceSettlement,
|
|
17
|
+
writeMaintenancePending,
|
|
18
|
+
} from "./entity-migration-maintenance-pending.js";
|
|
13
19
|
|
|
14
20
|
export interface EntityCanonicalIdMigrationHost {
|
|
15
21
|
readonly baseDir: string;
|
|
@@ -31,7 +37,13 @@ export interface EntityCanonicalIdMigrationHost {
|
|
|
31
37
|
invalidateColdMemoriesCache(): void;
|
|
32
38
|
bumpMemoryStatusVersion(): void;
|
|
33
39
|
getEntityMutationVersion(): number;
|
|
34
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Ask search maintenance to re-index. `false` means the request was refused
|
|
42
|
+
* (e.g. the host is shutting down) and a promise settling to `false` means no
|
|
43
|
+
* pass served it: the pending markers stay so the next start retries. A
|
|
44
|
+
* promise is awaited off the startup path; `true` or nothing clears them.
|
|
45
|
+
*/
|
|
46
|
+
onEntityMigrationComplete?: () => boolean | Promise<boolean> | void;
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
export async function runLegacyEntityCanonicalIdMigration(
|
|
@@ -43,15 +55,26 @@ export async function runLegacyEntityCanonicalIdMigration(
|
|
|
43
55
|
const readMigrationFingerprint = () =>
|
|
44
56
|
getFingerprint(host.baseDir, host.entitiesDir, () => String(host.getEntityMutationVersion()));
|
|
45
57
|
// The migration bumps the status version only after a pass that rewrote
|
|
46
|
-
// references; a no-op replay must not ask search maintenance to run
|
|
58
|
+
// references; a no-op replay must not ask search maintenance to run unless a
|
|
59
|
+
// previous run died owing it (issue #3286 pending marker).
|
|
47
60
|
let rewroteReferences = false;
|
|
61
|
+
// Markers an earlier run left behind: this run owes (and may clear) exactly
|
|
62
|
+
// these plus its own, never a marker a concurrent run arms meanwhile.
|
|
63
|
+
const owed = await listMaintenancePending(host.stateDir);
|
|
64
|
+
// Arm the marker before this run rewrites anything, so a crash between the
|
|
65
|
+
// rewrite and the hook leaves it behind. Idempotent per run.
|
|
66
|
+
let armed: Promise<string> | undefined;
|
|
67
|
+
const armPending = (): Promise<string> => (armed ??= writeMaintenancePending(host.stateDir));
|
|
48
68
|
const completionFingerprint = await migrateLegacyEntityCanonicalIds({
|
|
49
69
|
stateDir: host.stateDir,
|
|
50
70
|
entitiesDir: host.entitiesDir,
|
|
51
71
|
normalizeEntityName: host.normalizeEntityName.bind(host),
|
|
52
72
|
resolveEntityFilePath: host.resolveEntityFilePath.bind(host),
|
|
53
73
|
readStorageSecureFile: host.readStorageSecureFile.bind(host),
|
|
54
|
-
writeStorageSecureFile:
|
|
74
|
+
writeStorageSecureFile: async (filePath, content, forceEncrypt) => {
|
|
75
|
+
await armPending();
|
|
76
|
+
return host.writeStorageSecureFile(filePath, content, forceEncrypt);
|
|
77
|
+
},
|
|
55
78
|
isEncryptedStorageFile: host.isEncryptedFileHeader.bind(host),
|
|
56
79
|
snapshotBeforeWrite: host.snapshotBeforeWrite.bind(host),
|
|
57
80
|
parseEntityFile,
|
|
@@ -63,10 +86,14 @@ export async function runLegacyEntityCanonicalIdMigration(
|
|
|
63
86
|
readArchivedMemories: host.readArchivedMemories.bind(host),
|
|
64
87
|
validateMemoryScanRoots: () =>
|
|
65
88
|
validateRoots(host.baseDir, host.entitiesDir, host.stateDir, host.archiveDir),
|
|
66
|
-
rewriteProjectedEntityReferences: (mappings) =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
rewriteProjectedEntityReferences: async (mappings) => {
|
|
90
|
+
await armPending();
|
|
91
|
+
return rewriteProjectedEntityReferences(host.baseDir, mappings);
|
|
92
|
+
},
|
|
93
|
+
rewriteProjectedMemoryEntityReference: async (memoryId, previousEntityRef, nextEntityRef) => {
|
|
94
|
+
await armPending();
|
|
95
|
+
return rewriteProjectedMemoryEntityReference(host.baseDir, memoryId, previousEntityRef, nextEntityRef);
|
|
96
|
+
},
|
|
70
97
|
invalidateKnowledgeIndexCache: host.invalidateKnowledgeIndexCache.bind(host),
|
|
71
98
|
invalidateAllMemoriesCache: host.invalidateAllMemoriesCache.bind(host),
|
|
72
99
|
invalidateColdMemoriesCache: host.invalidateColdMemoriesCache.bind(host),
|
|
@@ -76,9 +103,17 @@ export async function runLegacyEntityCanonicalIdMigration(
|
|
|
76
103
|
},
|
|
77
104
|
readMigrationFingerprint,
|
|
78
105
|
} satisfies EntityCanonicalIdMigrationDependencies);
|
|
79
|
-
if (completionFingerprint !== undefined && rewroteReferences) {
|
|
106
|
+
if (completionFingerprint !== undefined && (rewroteReferences || owed.length > 0) && host.onEntityMigrationComplete) {
|
|
107
|
+
// No hook wired (e.g. a CLI-only process) skips this block, keeping the markers too.
|
|
108
|
+
const held = armed ? [...owed, await armed] : owed;
|
|
80
109
|
try {
|
|
81
|
-
host.onEntityMigrationComplete
|
|
110
|
+
const outcome = host.onEntityMigrationComplete();
|
|
111
|
+
const settling = settleMaintenancePending(host.stateDir, held, outcome).catch(() => {
|
|
112
|
+
log.warn("entity migration: search maintenance scheduling failed");
|
|
113
|
+
});
|
|
114
|
+
// An async hook is served after a debounce: never hold startup for it.
|
|
115
|
+
if (outcome instanceof Promise) trackMaintenanceSettlement(host.stateDir, settling);
|
|
116
|
+
else await settling;
|
|
82
117
|
} catch {
|
|
83
118
|
log.warn("entity migration: search maintenance scheduling failed");
|
|
84
119
|
}
|