@remnic/core 10.0.52 → 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-UM3ZLT2X.js → chunk-CA6X37OB.js} +31 -28
- 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/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/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.ts +8 -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-UM3ZLT2X.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
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { resolveNamespaceCapabilities,
|
|
23
23
|
resolveQmdCapabilities,resolveConsolidationCapabilities, resolveRecallAuxiliaryCapabilities } from "../capabilities.js";
|
|
24
24
|
import { existsSync, type Dirent } from "node:fs";
|
|
25
|
-
import {
|
|
25
|
+
import { lstat, readdir, realpath, stat } from "node:fs/promises";
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
|
|
28
28
|
import type { PluginConfig } from "../types.js";
|
|
@@ -57,11 +57,8 @@ import {
|
|
|
57
57
|
maybeRebuildMemoryProjectionScheduled,
|
|
58
58
|
terminateScheduledProjectionRebuilds,
|
|
59
59
|
} from "./projection-rebuild-schedule.js";
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
pendingLifecycleLedgerDir,
|
|
63
|
-
type LifecyclePendingIo,
|
|
64
|
-
} from "../storage/memory-lifecycle-ledger-access.js";
|
|
60
|
+
import { drainPendingLifecycleAppendsForPath } from "./lifecycle-pending-fallback-drain.js";
|
|
61
|
+
import { QmdMaintenanceCompletion } from "./qmd-maintenance-completion.js";
|
|
65
62
|
import { STATE_FILE_MAX_DECRYPT_BYTES } from "../storage/secure-line-reader.js";
|
|
66
63
|
import { createCompoundingWeeklyCron, type CompoundingWeeklySchedulerHandle } from "../compounding/weekly-scheduler.js";
|
|
67
64
|
import { startLocationAutoSync, type LocationAutoSyncHandle } from "../location/scheduler.js";
|
|
@@ -70,7 +67,7 @@ import type { ActivitySyncRunSummary } from "../activity/runner.js";
|
|
|
70
67
|
import { resolveHomeDir } from "../runtime/env.js";
|
|
71
68
|
import { log } from "../logger.js";
|
|
72
69
|
import { isErrnoCode } from "../utils/errno.js";
|
|
73
|
-
import { assertPathInsideRoot
|
|
70
|
+
import { assertPathInsideRoot } from "../utils/path-containment.js";
|
|
74
71
|
import {
|
|
75
72
|
probeEncryptedRegularFileHeader,
|
|
76
73
|
SECURE_STORE_ENVELOPE_OVERHEAD_BYTES,
|
|
@@ -137,6 +134,9 @@ export class MaintenanceScheduler {
|
|
|
137
134
|
private qmdMaintenanceTimer: NodeJS.Timeout | null = null;
|
|
138
135
|
private qmdMaintenancePending = false;
|
|
139
136
|
private qmdMaintenanceInFlight = false;
|
|
137
|
+
private readonly qmdCompletion = new QmdMaintenanceCompletion();
|
|
138
|
+
/** Namespaces that ran in passes of the request still waiting for full coverage. */
|
|
139
|
+
private readonly qmdCoveredNamespaces = new Set<string>();
|
|
140
140
|
private lastQmdEmbedAtMs = 0;
|
|
141
141
|
private lastQmdEmbedAtMsByNamespace = new Map<string, number>();
|
|
142
142
|
|
|
@@ -476,13 +476,13 @@ export class MaintenanceScheduler {
|
|
|
476
476
|
// QMD maintenance scheduling (debounced + singleflight)
|
|
477
477
|
// ───────────────────────────────────────────────────────────────────────
|
|
478
478
|
|
|
479
|
-
/** Internal: queue a debounced QMD maintenance pass. */
|
|
480
|
-
private requestQmdMaintenance():
|
|
481
|
-
if (!this.deps.getQmd().isAvailable()) return;
|
|
482
|
-
if (!resolveQmdCapabilities(this.deps.config).qmdMaintenance) return;
|
|
479
|
+
/** Internal: queue a debounced QMD maintenance pass; false when nothing could be armed. */
|
|
480
|
+
private requestQmdMaintenance(): boolean {
|
|
481
|
+
if (!this.deps.getQmd().isAvailable()) return false;
|
|
482
|
+
if (!resolveQmdCapabilities(this.deps.config).qmdMaintenance) return false;
|
|
483
483
|
|
|
484
484
|
this.qmdMaintenancePending = true;
|
|
485
|
-
if (this.qmdMaintenanceTimer) return;
|
|
485
|
+
if (this.qmdMaintenanceTimer) return true;
|
|
486
486
|
|
|
487
487
|
this.qmdMaintenanceTimer = setTimeout(() => {
|
|
488
488
|
this.qmdMaintenanceTimer = null;
|
|
@@ -490,6 +490,18 @@ export class MaintenanceScheduler {
|
|
|
490
490
|
log.debug(`background qmd maintenance failed: ${err}`),
|
|
491
491
|
);
|
|
492
492
|
}, this.deps.config.qmdMaintenanceDebounceMs);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Like a maintenance request, but resolves true only once a pass that started
|
|
498
|
+
* after it completed successfully. False when nothing could be armed (QMD
|
|
499
|
+
* unavailable or maintenance off), the pass failed, or dispose() abandoned it,
|
|
500
|
+
* so a caller that must not lose the re-index keeps owing it.
|
|
501
|
+
*/
|
|
502
|
+
requestQmdMaintenanceCompletion(): Promise<boolean> {
|
|
503
|
+
if (this.disposed || !this.requestQmdMaintenance()) return Promise.resolve(false);
|
|
504
|
+
return this.qmdCompletion.wait();
|
|
493
505
|
}
|
|
494
506
|
|
|
495
507
|
/**
|
|
@@ -530,6 +542,10 @@ export class MaintenanceScheduler {
|
|
|
530
542
|
if (!this.qmdMaintenancePending) return;
|
|
531
543
|
this.qmdMaintenanceInFlight = true;
|
|
532
544
|
this.qmdMaintenancePending = false;
|
|
545
|
+
// Waiters claimed here are served by THIS pass; later requests wait for the next.
|
|
546
|
+
const settle = this.qmdCompletion.claim();
|
|
547
|
+
let served = false;
|
|
548
|
+
let deferred = false;
|
|
533
549
|
|
|
534
550
|
try {
|
|
535
551
|
if (resolveNamespaceCapabilities(this.deps.config).namespaces) {
|
|
@@ -555,7 +571,7 @@ export class MaintenanceScheduler {
|
|
|
555
571
|
}
|
|
556
572
|
this.lastQmdEmbedAtMs = now;
|
|
557
573
|
};
|
|
558
|
-
await runNamespaceMaintenanceBatchPlan(
|
|
574
|
+
const summary = await runNamespaceMaintenanceBatchPlan(
|
|
559
575
|
this.deps.config,
|
|
560
576
|
plan,
|
|
561
577
|
async (candidates) => {
|
|
@@ -605,8 +621,46 @@ export class MaintenanceScheduler {
|
|
|
605
621
|
skipReasonForError: qmdMaintenanceSkipReasonForError,
|
|
606
622
|
},
|
|
607
623
|
);
|
|
624
|
+
// Namespaces skipped for a TRANSIENT reason (budget_exhausted, lock_held) were not covered
|
|
625
|
+
// by this pass. They are covered once they ran in this or an earlier pass of the same
|
|
626
|
+
// request, so a deployment with more namespaces than the cycle budget still converges.
|
|
627
|
+
// Permanent skip reasons describe namespaces this deployment never re-indexes.
|
|
628
|
+
const covered = this.qmdCoveredNamespaces;
|
|
629
|
+
const before = covered.size;
|
|
630
|
+
for (const status of summary.statuses) {
|
|
631
|
+
if (status.state === "ran") covered.add(status.namespace);
|
|
632
|
+
}
|
|
633
|
+
const uncovered = summary.statuses.some(
|
|
634
|
+
(status) =>
|
|
635
|
+
status.state === "skipped" &&
|
|
636
|
+
(status.reason === "budget_exhausted" || status.reason === "lock_held") &&
|
|
637
|
+
!covered.has(status.namespace),
|
|
638
|
+
);
|
|
639
|
+
served = summary.ran > 0 && summary.failed === 0 && !uncovered;
|
|
640
|
+
// Partial progress keeps the waiters for another pass; no progress (or a failure) ends the
|
|
641
|
+
// request unserved so a stuck namespace cannot re-arm passes forever.
|
|
642
|
+
deferred = !served && summary.failed === 0 && covered.size > before && !this.disposed;
|
|
608
643
|
} else {
|
|
609
|
-
|
|
644
|
+
const qmd = this.deps.getQmd();
|
|
645
|
+
// Plain update() is fail-open on every backend: it resolves when the min-interval gate,
|
|
646
|
+
// failure backoff or an early exit suppressed it, and when it errored. Only the variants
|
|
647
|
+
// that report whether an update ran count (updateCompleted, else QMD's updateStrict).
|
|
648
|
+
if (qmd.updateCompleted) {
|
|
649
|
+
try {
|
|
650
|
+
served = await qmd.updateCompleted();
|
|
651
|
+
} catch (err) {
|
|
652
|
+
log.debug(`qmd maintenance update did not run: ${err}`);
|
|
653
|
+
}
|
|
654
|
+
} else if (qmd.updateStrict) {
|
|
655
|
+
try {
|
|
656
|
+
await qmd.updateStrict();
|
|
657
|
+
served = true;
|
|
658
|
+
} catch (err) {
|
|
659
|
+
log.debug(`qmd maintenance update did not run: ${err}`);
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
await qmd.update();
|
|
663
|
+
}
|
|
610
664
|
const now = Date.now();
|
|
611
665
|
if (
|
|
612
666
|
resolveQmdCapabilities(this.deps.config).qmdAutoEmbed &&
|
|
@@ -624,7 +678,10 @@ export class MaintenanceScheduler {
|
|
|
624
678
|
// backoff) does not advance lastUpdateRanAtMs, so it leaves caches warm
|
|
625
679
|
// (#1904, Codex/Cursor).
|
|
626
680
|
} finally {
|
|
681
|
+
settle(served, deferred);
|
|
682
|
+
if (!deferred) this.qmdCoveredNamespaces.clear();
|
|
627
683
|
this.qmdMaintenanceInFlight = false;
|
|
684
|
+
if (deferred) this.qmdMaintenancePending = true;
|
|
628
685
|
if (this.qmdMaintenancePending) {
|
|
629
686
|
this.requestQmdMaintenance();
|
|
630
687
|
}
|
|
@@ -772,7 +829,7 @@ export class MaintenanceScheduler {
|
|
|
772
829
|
if (target.storage) {
|
|
773
830
|
await this.drainPendingLifecycleAppends(target.storage);
|
|
774
831
|
} else {
|
|
775
|
-
await
|
|
832
|
+
await drainPendingLifecycleAppendsForPath(target.memoryDir);
|
|
776
833
|
}
|
|
777
834
|
}
|
|
778
835
|
|
|
@@ -787,86 +844,6 @@ export class MaintenanceScheduler {
|
|
|
787
844
|
}
|
|
788
845
|
}
|
|
789
846
|
|
|
790
|
-
/**
|
|
791
|
-
* Drain a filesystem-fallback target's pending lifecycle-append spill when no
|
|
792
|
-
* secure StorageManager is available (catalog-disabled namespace; issue
|
|
793
|
-
* #2033). Uses the safe path-scoped PLAINTEXT context this fallback already
|
|
794
|
-
* operates in: a keyless drain can only fold plaintext spills into a plaintext
|
|
795
|
-
* ledger, so it refuses when the ledger or ANY spill is encrypted at rest,
|
|
796
|
-
* leaving those pending for a keyed (catalog-path) drain — exactly mirroring
|
|
797
|
-
* the fallback compaction's encrypted-ledger deferral. Non-fatal; a fast
|
|
798
|
-
* no-op when nothing is pending. The shared spill lister enforces the symlink/
|
|
799
|
-
* containment guard on every spill file it reads or deletes.
|
|
800
|
-
*/
|
|
801
|
-
private async drainPendingLifecycleAppendsForPath(memoryDir: string): Promise<void> {
|
|
802
|
-
const ledgerPath = path.join(memoryDir, "state", "memory-lifecycle-ledger.jsonl");
|
|
803
|
-
const spillDir = pendingLifecycleLedgerDir(ledgerPath);
|
|
804
|
-
// Enumerate spills through the SAME guarded lister the actual drain uses
|
|
805
|
-
// (listContainedSpillFiles): it rejects a symlinked/non-directory spill dir
|
|
806
|
-
// and skips any symlink/FIFO/device/escaping entry, returning only regular
|
|
807
|
-
// files contained in the dir. Never raw-readdir + open() an untrusted name:
|
|
808
|
-
// opening a FIFO would block until a writer appears and a symlink would
|
|
809
|
-
// redirect the probe outside the dir (#2033). Enumerate BOTH live `*.jsonl`
|
|
810
|
-
// spills and crash-orphaned `*.jsonl.claimed` files: a drain that died
|
|
811
|
-
// between claiming a spill (rename to `.claimed`) and committing it leaves
|
|
812
|
-
// the ONLY copy of those rows as a claimed orphan. Gating solely on live
|
|
813
|
-
// spills would early-return here whenever the pending dir holds nothing but
|
|
814
|
-
// orphans, stranding those rows forever; proceeding lets
|
|
815
|
-
// drainPendingLifecycleLedgerIfAny -> recoverOrphanedClaims restore and
|
|
816
|
-
// re-commit them under the ledger lock (#2033).
|
|
817
|
-
let spillFiles: string[];
|
|
818
|
-
let claimedOrphans: string[];
|
|
819
|
-
try {
|
|
820
|
-
spillFiles = await listContainedSpillFiles(spillDir);
|
|
821
|
-
claimedOrphans = await listContainedSpillFiles(spillDir, ".jsonl.claimed");
|
|
822
|
-
} catch (err) {
|
|
823
|
-
log.debug(`fallback lifecycle pending scan failed (non-fatal) for ${memoryDir}: ${err}`);
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
if (spillFiles.length === 0 && claimedOrphans.length === 0) return;
|
|
827
|
-
// The pending encryption state is authoritative only UNDER the ledger lock:
|
|
828
|
-
// drainPendingLifecycleLedgerIfAny waits for the lock and re-enumerates the
|
|
829
|
-
// pending dir, so a secure-store writer that spills an encrypted file — or a
|
|
830
|
-
// keyed compaction that rewrites the ledger encrypted — while this fallback
|
|
831
|
-
// waits would defeat any pre-lock probe (#2033). Probe each file at the
|
|
832
|
-
// moment of use inside the held lock instead: readSecure refuses an
|
|
833
|
-
// encrypted spill (the fold reads each spill lazily and propagates the throw
|
|
834
|
-
// BEFORE claiming that spill, leaving it intact) and the append refuses a
|
|
835
|
-
// ledger that became encrypted (the fold rolls the claims back). Either way
|
|
836
|
-
// the plaintext IO
|
|
837
|
-
// never reads or writes ciphertext; the keyed catalog-path drain handles the
|
|
838
|
-
// deferred rows.
|
|
839
|
-
const io: LifecyclePendingIo = {
|
|
840
|
-
writeSecure: async (filePath, payload) => {
|
|
841
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
842
|
-
await writeFile(filePath, payload, "utf-8");
|
|
843
|
-
},
|
|
844
|
-
readSecure: async (filePath) => {
|
|
845
|
-
if (await probeEncryptedRegularFileHeader(filePath)) {
|
|
846
|
-
throw new Error(`fallback lifecycle drain deferred: encrypted spill at ${filePath}`);
|
|
847
|
-
}
|
|
848
|
-
return readFile(filePath, "utf-8");
|
|
849
|
-
},
|
|
850
|
-
};
|
|
851
|
-
try {
|
|
852
|
-
await drainPendingLifecycleLedgerIfAny(
|
|
853
|
-
ledgerPath,
|
|
854
|
-
io,
|
|
855
|
-
async (payload) => {
|
|
856
|
-
if (await probeEncryptedRegularFileHeader(ledgerPath)) {
|
|
857
|
-
throw new Error(`fallback lifecycle drain deferred: ledger became encrypted at ${ledgerPath}`);
|
|
858
|
-
}
|
|
859
|
-
await appendFile(ledgerPath, payload, "utf-8");
|
|
860
|
-
},
|
|
861
|
-
async () => {
|
|
862
|
-
await mkdir(path.dirname(ledgerPath), { recursive: true });
|
|
863
|
-
},
|
|
864
|
-
);
|
|
865
|
-
} catch (err) {
|
|
866
|
-
log.debug(`fallback lifecycle pending drain failed (non-fatal) for ${memoryDir}: ${err}`);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
847
|
/**
|
|
871
848
|
* Enumerate the lifecycle ledgers to consider for compaction: the root
|
|
872
849
|
* (through the secure-configured root storage when available) plus every
|
|
@@ -1186,6 +1163,7 @@ export class MaintenanceScheduler {
|
|
|
1186
1163
|
this.qmdMaintenanceTimer = null;
|
|
1187
1164
|
}
|
|
1188
1165
|
this.qmdMaintenancePending = false;
|
|
1166
|
+
this.qmdCompletion.abandon();
|
|
1189
1167
|
await activityDrain;
|
|
1190
1168
|
await locationDrain; await weeklyDrain;
|
|
1191
1169
|
// Belt-and-suspenders: clear anything a late tick armed before the latch.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Callers that must know the re-index actually ran (the entity-migration
|
|
3
|
+
* pending marker, issue #3286) cannot rely on arming the debounced timer: a
|
|
4
|
+
* crash or dispose() inside the debounce window would lose the request. They
|
|
5
|
+
* wait here instead; the scheduler claims the waiters when a pass starts and
|
|
6
|
+
* settles them with the pass outcome, and abandons them on dispose().
|
|
7
|
+
*/
|
|
8
|
+
export class QmdMaintenanceCompletion {
|
|
9
|
+
private waiters: Array<(served: boolean) => void> = [];
|
|
10
|
+
|
|
11
|
+
/** Resolves true when a pass that started after this call completed successfully. */
|
|
12
|
+
wait(): Promise<boolean> {
|
|
13
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A pass is starting: take every waiter it will serve and return their settle function.
|
|
18
|
+
* `defer` keeps them waiting for a later pass (a pass that covered only part of the selection).
|
|
19
|
+
*/
|
|
20
|
+
claim(): (served: boolean, defer?: boolean) => void {
|
|
21
|
+
const claimed = this.waiters.splice(0);
|
|
22
|
+
return (served, defer = false) => {
|
|
23
|
+
if (defer) this.waiters.unshift(...claimed);
|
|
24
|
+
else claimed.forEach((resolve) => resolve(served));
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The scheduler stopped: nothing will serve the waiters. */
|
|
29
|
+
abandon(): void {
|
|
30
|
+
this.claim()(false);
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -1512,7 +1512,14 @@ export class Orchestrator {
|
|
|
1512
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)}`)); },
|
|
1513
1513
|
});
|
|
1514
1514
|
const bindStorage = (storage: StorageManager, namespace: string) => {
|
|
1515
|
-
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
|
+
};
|
|
1516
1523
|
if (this.prioritizedEmbedding) storage.onMemoryWrite = (filePath) => this.prioritizedEmbedding?.enqueue(filePath, namespace);
|
|
1517
1524
|
};
|
|
1518
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
|
|