@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
import { SmartBuffer } from "./buffer.js";
|
|
6
|
+
import { resetExtractionLlmHealthForTests } from "./extraction-llm-health.js";
|
|
7
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
8
|
+
import { ContentHashIndex } from "./storage.js";
|
|
9
|
+
import {
|
|
10
|
+
cleanupDir,
|
|
11
|
+
harmonicNodeStoreStatus,
|
|
12
|
+
makeHarmonicLifecycleConfig,
|
|
13
|
+
makeLifecycleConfig,
|
|
14
|
+
mkTempMemoryDir,
|
|
15
|
+
stubExtraction,
|
|
16
|
+
} from "./testing/orchestrator-lite.js";
|
|
17
|
+
import type { ExtractionResult } from "./types.js";
|
|
18
|
+
|
|
19
|
+
/** Owner contract for the private Orchestrator temporal/tag batch hook. */
|
|
20
|
+
const FACT = "Project Alpha uses a queue for background processing.";
|
|
21
|
+
|
|
22
|
+
for (const includeFact of [false, true]) {
|
|
23
|
+
test(`#3228: entity write failure retains input${includeFact ? " after a committed fact" : ""}`, async () => {
|
|
24
|
+
const memoryDir = await mkTempMemoryDir("entity-write-failure");
|
|
25
|
+
const config = makeLifecycleConfig(memoryDir, {
|
|
26
|
+
chunkingEnabled: false,
|
|
27
|
+
multiGraphMemoryEnabled: false,
|
|
28
|
+
factDeduplicationEnabled: true,
|
|
29
|
+
extractionMinImportanceLevel: "trivial",
|
|
30
|
+
entityActivityLogEnabled: false,
|
|
31
|
+
});
|
|
32
|
+
const orchestrator = new Orchestrator(config);
|
|
33
|
+
const storage = await orchestrator.getStorageForNamespace();
|
|
34
|
+
const writeEntity = storage.writeEntity.bind(storage);
|
|
35
|
+
try {
|
|
36
|
+
await storage.ensureDirectories();
|
|
37
|
+
const entityName = storage.normalizeEntityName("Alpha", "project");
|
|
38
|
+
await orchestrator.buffer.load();
|
|
39
|
+
const bufferKey = "entity-write-failure";
|
|
40
|
+
await orchestrator.buffer.addTurn(bufferKey, {
|
|
41
|
+
role: "user",
|
|
42
|
+
content: FACT,
|
|
43
|
+
sessionKey: bufferKey,
|
|
44
|
+
timestamp: "2026-10-01T12:00:00.000Z",
|
|
45
|
+
sourceValidAt: "2026-10-01T12:00:00.000Z",
|
|
46
|
+
persistProcessedFingerprint: true,
|
|
47
|
+
});
|
|
48
|
+
await orchestrator.buffer.flushPendingSave({ throwOnFailure: true });
|
|
49
|
+
const turns = orchestrator.buffer.getTurns(bufferKey);
|
|
50
|
+
assert.equal(turns.length, 1, "the source turn must be durably buffered before extraction");
|
|
51
|
+
const before = await storage.loadMeta();
|
|
52
|
+
const extractionCalls = stubExtraction(orchestrator, (): ExtractionResult => ({
|
|
53
|
+
facts: includeFact
|
|
54
|
+
? [{ content: FACT, category: "fact", confidence: 0.9, tags: [] }]
|
|
55
|
+
: [],
|
|
56
|
+
entities: [{ name: "Alpha", type: "project", facts: ["Uses a queue"] }],
|
|
57
|
+
questions: [],
|
|
58
|
+
profileUpdates: [],
|
|
59
|
+
}));
|
|
60
|
+
const writeFailure = Object.assign(new Error("synthetic entity write denied"), { code: "EACCES" });
|
|
61
|
+
let entityWriteAttempts = 0;
|
|
62
|
+
storage.writeEntity = async (...args: Parameters<typeof writeEntity>) => {
|
|
63
|
+
entityWriteAttempts += 1;
|
|
64
|
+
assert.deepEqual(args.slice(0, 3), ["Alpha", "project", ["Uses a queue"]]);
|
|
65
|
+
throw writeFailure;
|
|
66
|
+
};
|
|
67
|
+
let commits = 0;
|
|
68
|
+
const options = { bufferKey, onDurableCommit: () => { commits += 1; } };
|
|
69
|
+
|
|
70
|
+
await assert.rejects(
|
|
71
|
+
orchestrator.runExtraction(turns, options),
|
|
72
|
+
(error: Error) => error === writeFailure,
|
|
73
|
+
"the original entity I/O failure must reach the caller",
|
|
74
|
+
);
|
|
75
|
+
assert.equal(entityWriteAttempts, 1, "the failure must come from the real entity persistence path");
|
|
76
|
+
assert.equal(commits, 0, "a failed batch must not signal durable completion");
|
|
77
|
+
assert.deepEqual(orchestrator.buffer.getTurns(bufferKey), turns);
|
|
78
|
+
const retainedBuffer = new SmartBuffer(config, storage);
|
|
79
|
+
await retainedBuffer.load();
|
|
80
|
+
assert.deepEqual(retainedBuffer.getTurns(bufferKey), turns, "source turns must remain on disk too");
|
|
81
|
+
const failedMeta = await storage.loadMeta();
|
|
82
|
+
assert.equal(failedMeta.processedExtractionFingerprints?.length ?? 0, 0);
|
|
83
|
+
assert.equal(failedMeta.extractionCount, before.extractionCount);
|
|
84
|
+
assert.equal(await storage.readEntity(entityName), "", "the failed entity was not written");
|
|
85
|
+
|
|
86
|
+
const factsBeforeRetry = (await storage.readAllMemories()).filter((m) => m.frontmatter.category === "fact");
|
|
87
|
+
assert.equal(factsBeforeRetry.length, includeFact ? 1 : 0);
|
|
88
|
+
if (includeFact) {
|
|
89
|
+
const hashes = new ContentHashIndex(path.join(storage.dir, "state"));
|
|
90
|
+
await hashes.load();
|
|
91
|
+
assert.equal(hashes.has(FACT), true, "the earlier fact's deferred hash must be durable before rejection");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
storage.writeEntity = writeEntity;
|
|
95
|
+
const retried = await orchestrator.runExtraction(turns, options);
|
|
96
|
+
assert.equal(retried.status, "completed");
|
|
97
|
+
assert.equal(retried.persistedCount, 1, "retry writes the entity, not another copy of an earlier fact");
|
|
98
|
+
assert.equal(commits, 1);
|
|
99
|
+
assert.match(await storage.readEntity(entityName), /Uses a queue/);
|
|
100
|
+
assert.equal((await storage.listEntityNames()).length, 1);
|
|
101
|
+
const factsAfterRetry = (await storage.readAllMemories()).filter((m) => m.frontmatter.category === "fact");
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
factsAfterRetry.map((m) => m.frontmatter.id),
|
|
104
|
+
factsBeforeRetry.map((m) => m.frontmatter.id),
|
|
105
|
+
"retry must preserve the already committed fact without duplicating it",
|
|
106
|
+
);
|
|
107
|
+
assert.equal((await storage.loadMeta()).processedExtractionFingerprints?.length, 1);
|
|
108
|
+
assert.equal(orchestrator.buffer.getTurns(bufferKey).length, 0);
|
|
109
|
+
const clearedBuffer = new SmartBuffer(config, storage);
|
|
110
|
+
await clearedBuffer.load();
|
|
111
|
+
assert.equal(clearedBuffer.getTurns(bufferKey).length, 0);
|
|
112
|
+
assert.equal((await orchestrator.runExtraction(turns, options)).reason, "processed_fingerprint");
|
|
113
|
+
assert.equal(extractionCalls.length, 2, "only the failed attempt and successful retry call the extractor");
|
|
114
|
+
assert.equal(commits, 1, "processed replay must not emit a second commit");
|
|
115
|
+
} finally {
|
|
116
|
+
storage.writeEntity = writeEntity;
|
|
117
|
+
try {
|
|
118
|
+
await orchestrator.destroy();
|
|
119
|
+
} finally {
|
|
120
|
+
resetExtractionLlmHealthForTests();
|
|
121
|
+
await cleanupDir(memoryDir);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
test("#3231: entity failure after a committed fact still runs deferred post-write maintenance", async () => {
|
|
128
|
+
// Partial-success path: fact (preference) is durable, later writeEntity throws.
|
|
129
|
+
// Batch-end finalization in persistExtraction (hash flush, behavior ledger,
|
|
130
|
+
// harmonic construction, temporal/tag index) and runExtraction's
|
|
131
|
+
// requestQmdMaintenance must still cover the earlier durable write. Retry
|
|
132
|
+
// exact-dedupes the fact (`continue`), so those effects are not re-queued.
|
|
133
|
+
const memoryDir = await mkTempMemoryDir("entity-write-failure-maintenance");
|
|
134
|
+
const preference =
|
|
135
|
+
"Prefers terse queue names for background jobs in Project Alpha.";
|
|
136
|
+
const config = makeHarmonicLifecycleConfig(memoryDir, {
|
|
137
|
+
chunkingEnabled: false,
|
|
138
|
+
multiGraphMemoryEnabled: false,
|
|
139
|
+
factDeduplicationEnabled: true,
|
|
140
|
+
extractionMinImportanceLevel: "trivial",
|
|
141
|
+
entityActivityLogEnabled: false,
|
|
142
|
+
queryAwareIndexingEnabled: true,
|
|
143
|
+
});
|
|
144
|
+
const orchestrator = new Orchestrator(config);
|
|
145
|
+
const storage = await orchestrator.getStorageForNamespace();
|
|
146
|
+
const writeEntity = storage.writeEntity.bind(storage);
|
|
147
|
+
const temporalIdBatches: string[][] = [];
|
|
148
|
+
// Bracket access is TypeScript's typed escape hatch for the private method; the spy only wraps and restores it.
|
|
149
|
+
const originalTemporal = orchestrator["updateTemporalTagIndexes"].bind(orchestrator);
|
|
150
|
+
orchestrator["updateTemporalTagIndexes"] = async (target, persistedIds) => {
|
|
151
|
+
temporalIdBatches.push([...persistedIds]);
|
|
152
|
+
return originalTemporal(target, persistedIds);
|
|
153
|
+
};
|
|
154
|
+
let qmdMaintenanceCalls = 0;
|
|
155
|
+
const originalQmd = orchestrator.requestQmdMaintenance.bind(orchestrator);
|
|
156
|
+
orchestrator.requestQmdMaintenance = () => {
|
|
157
|
+
qmdMaintenanceCalls += 1;
|
|
158
|
+
originalQmd();
|
|
159
|
+
};
|
|
160
|
+
try {
|
|
161
|
+
await storage.ensureDirectories();
|
|
162
|
+
await orchestrator.buffer.load();
|
|
163
|
+
const bufferKey = "entity-write-failure-maintenance";
|
|
164
|
+
await orchestrator.buffer.addTurn(bufferKey, {
|
|
165
|
+
role: "user",
|
|
166
|
+
content: preference,
|
|
167
|
+
sessionKey: bufferKey,
|
|
168
|
+
timestamp: "2026-10-01T12:00:00.000Z",
|
|
169
|
+
sourceValidAt: "2026-10-01T12:00:00.000Z",
|
|
170
|
+
persistProcessedFingerprint: true,
|
|
171
|
+
});
|
|
172
|
+
await orchestrator.buffer.flushPendingSave({ throwOnFailure: true });
|
|
173
|
+
const turns = orchestrator.buffer.getTurns(bufferKey);
|
|
174
|
+
assert.equal(turns.length, 1);
|
|
175
|
+
stubExtraction(orchestrator, (): ExtractionResult => ({
|
|
176
|
+
facts: [
|
|
177
|
+
{
|
|
178
|
+
content: preference,
|
|
179
|
+
category: "preference",
|
|
180
|
+
confidence: 0.95,
|
|
181
|
+
tags: ["queue-naming"],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
entities: [{ name: "Alpha", type: "project", facts: ["Uses a queue"] }],
|
|
185
|
+
questions: [],
|
|
186
|
+
profileUpdates: [],
|
|
187
|
+
}));
|
|
188
|
+
const writeFailure = Object.assign(new Error("synthetic entity write denied"), {
|
|
189
|
+
code: "EACCES",
|
|
190
|
+
});
|
|
191
|
+
storage.writeEntity = async () => {
|
|
192
|
+
throw writeFailure;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
await assert.rejects(
|
|
196
|
+
orchestrator.runExtraction(turns, { bufferKey }),
|
|
197
|
+
(error: Error) => error === writeFailure,
|
|
198
|
+
"entity I/O failure must still reach the caller",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const facts = (await storage.readAllMemories()).filter(
|
|
202
|
+
(m) => m.frontmatter.category === "preference",
|
|
203
|
+
);
|
|
204
|
+
assert.equal(facts.length, 1, "the preference fact must remain durable");
|
|
205
|
+
const factId = facts[0]!.frontmatter.id;
|
|
206
|
+
const hashes = new ContentHashIndex(path.join(storage.dir, "state"));
|
|
207
|
+
await hashes.load();
|
|
208
|
+
assert.equal(hashes.has(preference), true, "deferred fact hash must flush before rejection");
|
|
209
|
+
|
|
210
|
+
const signals = await storage.readBehaviorSignals(50);
|
|
211
|
+
assert.ok(
|
|
212
|
+
signals.some(
|
|
213
|
+
(s) => s.memoryId === factId && s.signalType === "preference_affinity",
|
|
214
|
+
),
|
|
215
|
+
"batch-end behavior-signal append must cover the committed preference before rethrow",
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const harmonic = await harmonicNodeStoreStatus(memoryDir);
|
|
219
|
+
assert.equal(harmonic.nodes.invalid, 0);
|
|
220
|
+
assert.ok(
|
|
221
|
+
harmonic.nodes.valid > 0 && (harmonic.latestNode?.sourceMemoryIds ?? []).includes(factId),
|
|
222
|
+
"batch-end harmonic construction must cover the committed fact before rethrow",
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
assert.ok(
|
|
226
|
+
temporalIdBatches.some((ids) => ids.includes(factId)),
|
|
227
|
+
"batch-end temporal/tag index update must include the committed fact before rethrow",
|
|
228
|
+
);
|
|
229
|
+
assert.ok(
|
|
230
|
+
qmdMaintenanceCalls > 0,
|
|
231
|
+
"partial durable writes must still schedule QMD reindex before the entity error propagates",
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// Retry exact-dedupes the preference (`continue`), so missing deferred
|
|
235
|
+
// effects are not re-queued. Prove the failure-path finalization is the
|
|
236
|
+
// only chance they have to land.
|
|
237
|
+
storage.writeEntity = writeEntity;
|
|
238
|
+
const retried = await orchestrator.runExtraction(turns, { bufferKey });
|
|
239
|
+
assert.equal(retried.status, "completed");
|
|
240
|
+
assert.equal(
|
|
241
|
+
(await storage.readAllMemories()).filter((m) => m.frontmatter.category === "preference").length,
|
|
242
|
+
1,
|
|
243
|
+
"retry must not duplicate the already-committed preference",
|
|
244
|
+
);
|
|
245
|
+
assert.ok(
|
|
246
|
+
(await storage.readBehaviorSignals(50)).some(
|
|
247
|
+
(s) => s.memoryId === factId && s.signalType === "preference_affinity",
|
|
248
|
+
),
|
|
249
|
+
"behavior signals must survive retry without relying on re-queue from the deduped fact",
|
|
250
|
+
);
|
|
251
|
+
assert.ok(
|
|
252
|
+
((await harmonicNodeStoreStatus(memoryDir)).latestNode?.sourceMemoryIds ?? []).includes(factId),
|
|
253
|
+
"harmonic records must survive retry without relying on re-queue from the deduped fact",
|
|
254
|
+
);
|
|
255
|
+
} finally {
|
|
256
|
+
storage.writeEntity = writeEntity;
|
|
257
|
+
orchestrator.requestQmdMaintenance = originalQmd;
|
|
258
|
+
orchestrator["updateTemporalTagIndexes"] = originalTemporal;
|
|
259
|
+
try {
|
|
260
|
+
await orchestrator.destroy();
|
|
261
|
+
} finally {
|
|
262
|
+
resetExtractionLlmHealthForTests();
|
|
263
|
+
await cleanupDir(memoryDir);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("#3231 P2: a post-write contradiction-resolve failure still finalizes the durable fact", async () => {
|
|
269
|
+
// The fact is durable (writeSealedMemory returned) when
|
|
270
|
+
// applyDeferredContradictionResolve rejects. Its behavior signal, harmonic
|
|
271
|
+
// node, temporal/tag index entry and QMD request must not depend on code
|
|
272
|
+
// that runs after that catch; retry exact-dedupes the fact.
|
|
273
|
+
const memoryDir = await mkTempMemoryDir("contradiction-resolve-failure-maintenance");
|
|
274
|
+
const preference = "Prefers terse queue names for background jobs in Project Beta.";
|
|
275
|
+
const config = makeHarmonicLifecycleConfig(memoryDir, {
|
|
276
|
+
chunkingEnabled: false,
|
|
277
|
+
multiGraphMemoryEnabled: false,
|
|
278
|
+
factDeduplicationEnabled: true,
|
|
279
|
+
extractionMinImportanceLevel: "trivial",
|
|
280
|
+
entityActivityLogEnabled: false,
|
|
281
|
+
queryAwareIndexingEnabled: true,
|
|
282
|
+
});
|
|
283
|
+
const orchestrator = new Orchestrator(config);
|
|
284
|
+
const storage = await orchestrator.getStorageForNamespace();
|
|
285
|
+
const temporalIdBatches: string[][] = [];
|
|
286
|
+
const originalTemporal = orchestrator["updateTemporalTagIndexes"].bind(orchestrator);
|
|
287
|
+
orchestrator["updateTemporalTagIndexes"] = async (target, persistedIds) => {
|
|
288
|
+
temporalIdBatches.push([...persistedIds]);
|
|
289
|
+
return originalTemporal(target, persistedIds);
|
|
290
|
+
};
|
|
291
|
+
let qmdMaintenanceCalls = 0;
|
|
292
|
+
const originalQmd = orchestrator.requestQmdMaintenance.bind(orchestrator);
|
|
293
|
+
orchestrator.requestQmdMaintenance = () => {
|
|
294
|
+
qmdMaintenanceCalls += 1;
|
|
295
|
+
originalQmd();
|
|
296
|
+
};
|
|
297
|
+
const originalResolve = orchestrator["applyDeferredContradictionResolve"].bind(orchestrator);
|
|
298
|
+
const resolveFailure = new Error("synthetic contradiction resolve failure");
|
|
299
|
+
orchestrator["applyDeferredContradictionResolve"] = async () => {
|
|
300
|
+
throw resolveFailure;
|
|
301
|
+
};
|
|
302
|
+
try {
|
|
303
|
+
await storage.ensureDirectories();
|
|
304
|
+
await orchestrator.buffer.load();
|
|
305
|
+
const bufferKey = "contradiction-resolve-failure-maintenance";
|
|
306
|
+
await orchestrator.buffer.addTurn(bufferKey, {
|
|
307
|
+
role: "user",
|
|
308
|
+
content: preference,
|
|
309
|
+
sessionKey: bufferKey,
|
|
310
|
+
timestamp: "2026-10-01T12:00:00.000Z",
|
|
311
|
+
sourceValidAt: "2026-10-01T12:00:00.000Z",
|
|
312
|
+
persistProcessedFingerprint: true,
|
|
313
|
+
});
|
|
314
|
+
await orchestrator.buffer.flushPendingSave({ throwOnFailure: true });
|
|
315
|
+
const turns = orchestrator.buffer.getTurns(bufferKey);
|
|
316
|
+
stubExtraction(orchestrator, (): ExtractionResult => ({
|
|
317
|
+
facts: [
|
|
318
|
+
{
|
|
319
|
+
content: preference,
|
|
320
|
+
category: "preference",
|
|
321
|
+
confidence: 0.95,
|
|
322
|
+
tags: ["queue-naming"],
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
entities: [],
|
|
326
|
+
questions: [],
|
|
327
|
+
profileUpdates: [],
|
|
328
|
+
}));
|
|
329
|
+
|
|
330
|
+
await assert.rejects(
|
|
331
|
+
orchestrator.runExtraction(turns, { bufferKey }),
|
|
332
|
+
(error: Error) => error === resolveFailure,
|
|
333
|
+
"the post-write failure must still reach the caller",
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const facts = (await storage.readAllMemories()).filter(
|
|
337
|
+
(m) => m.frontmatter.category === "preference",
|
|
338
|
+
);
|
|
339
|
+
assert.equal(facts.length, 1, "the preference fact must remain durable");
|
|
340
|
+
const factId = facts[0]!.frontmatter.id;
|
|
341
|
+
assert.ok(
|
|
342
|
+
(await storage.readBehaviorSignals(50)).some(
|
|
343
|
+
(s) => s.memoryId === factId && s.signalType === "preference_affinity",
|
|
344
|
+
),
|
|
345
|
+
"the durable fact's behavior signal must land before the rethrow",
|
|
346
|
+
);
|
|
347
|
+
assert.ok(
|
|
348
|
+
((await harmonicNodeStoreStatus(memoryDir)).latestNode?.sourceMemoryIds ?? []).includes(factId),
|
|
349
|
+
"the durable fact's harmonic node must land before the rethrow",
|
|
350
|
+
);
|
|
351
|
+
assert.ok(
|
|
352
|
+
temporalIdBatches.some((ids) => ids.includes(factId)),
|
|
353
|
+
"the durable fact's temporal/tag index update must run before the rethrow",
|
|
354
|
+
);
|
|
355
|
+
assert.ok(
|
|
356
|
+
qmdMaintenanceCalls > 0,
|
|
357
|
+
"a durable fact that is the only write must still schedule QMD reindex",
|
|
358
|
+
);
|
|
359
|
+
} finally {
|
|
360
|
+
orchestrator["applyDeferredContradictionResolve"] = originalResolve;
|
|
361
|
+
orchestrator.requestQmdMaintenance = originalQmd;
|
|
362
|
+
orchestrator["updateTemporalTagIndexes"] = originalTemporal;
|
|
363
|
+
try {
|
|
364
|
+
await orchestrator.destroy();
|
|
365
|
+
} finally {
|
|
366
|
+
resetExtractionLlmHealthForTests();
|
|
367
|
+
await cleanupDir(memoryDir);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|